Commit 770b0c75 authored by wangtao's avatar wangtao

Initial commit

parents
# 本地测试
#VITE_BASE_URL=http://192.168.4.94:8000
# 国内测试
#VITE_BASE_URL=https://pay-rys.zhubei.cn
# 海外测试
VITE_BASE_URL=http://127.0.0.1:8000
VITE_BASE_URL=https://api.bmiss.live
VITE_BASE_URL=
\ No newline at end of file
VITE_BASE_URL=https://api.bmiss3.zhubei.cn
components.d.ts
node_modules
\ No newline at end of file
module.exports = {
env: {
browser: true,
es2021: true
},
extends: ["eslint:recommended", "plugin:vue/vue3-essential", "plugin:@typescript-eslint/recommended"],
overrides: [
{
files: ["*.d.ts", "src/types/*", "src/**/apis.ts"],
rules: {
"no-unused-vars": 0
}
}
],
parser: "vue-eslint-parser",
parserOptions: {
ecmaVersion: "latest",
parser: "@typescript-eslint/parser",
sourceType: "module"
},
plugins: ["vue", "@typescript-eslint"],
rules: {
quotes: ["error", "double"],
semi: ["error", "always"],
"vue/valid-define-emits": 0,
"@typescript-eslint/ban-ts-comment": 0,
"vue/multi-word-component-names": 0,
"vue/no-mutating-props": 0,
"prefer-template": 2,
"@typescript-eslint/no-explicit-any": 2,
"linebreak-style": 0,
"no-debugger": 1,
"no-mixed-spaces-and-tabs": 0,
"vue/no-setup-props-destructure": 0,
"no-undef": 0,
"max-params": ["error", 3],
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_" // 使用正则匹配需要忽略的参数
}
]
}
};
\ No newline at end of file
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
invite-friends
# Editor directories and files
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
components.d.ts
.vscode
*-base
.eslintcache
/src/autoImport.d.ts
*.zip
production-*
standard-*
test-*
\ No newline at end of file
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Run eslint
pnpm lint-clear
# If eslint has errors or warnings, prevent the commit
if [ $? -gt 0 ]; then
echo "ESLint has errors or warnings. Please fix them before committing."
exit 1
fi
{
"semi": true,
"singleQuote": false,
"trailingComma": "none",
"bracketSpacing": true,
"arrowParens": "avoid",
"tabWidth": 4,
"printWidth": 150,
"proseWrap": "preserve",
"useTabs": true
}
\ No newline at end of file
#### 接口地址: https://console-docs.apipost.cn/preview/49d3a779f1442055/1aca158c259c744f?target_id=908272f8-83a9-4e0a-95f2-6190cdbed7f5
#### 主要采用vue ^3.2.45,vue-router ^4.1.6, eslint ^8.4.10, postcss-px-to-viewport ^1.1.1 , stylus ^0.59.0, vite ^4.0.0;
#### 已引入环境变量,如需修改,请自行修改;
#### 打包时只有在生产环境下关闭console输出,路由在所有环境为hash模式,路由baseurl为package.name,打包时关闭eslint代码检查,生产环境自动关闭vconsole调试工具
#### 所有全局interface均放在`src/interface`文件夹中
#### 所有全局type均放在`src/types`文件夹中
#### 所有全局实体类均放在 `src/entities`文件夹中
#### 路由创建在页面同级下创建 `pageConfig.ts` 请参考 `views/index/pageConfig.ts`
#### 自动引入vue相关的函数, 具体函数详情见 `autoImport.d.ts`
#### !!!重要: 代码提交请执行 npm run commit 然后按照实际情况进行选择, 代码会自动格式化
#### 页面布局中如果使用components/shell 作为根组件,样式需要进行 props 传递
#### 简介:
##### utils中的 HandleApp 是与 app端 交互的方法, 里面包含了当前的设备类型 如果是ios则也会包含ios的版本信息等
##### utils中的 useCookie 是为了获取 cookie中存储的数据, 如果有userid 则会被base64 编码
##### useCrypto中 有加密和解密方法(不是请求的,请求的加解密是另外一个) ,一般会用登录成功之后 url地址上的token进行解密 或者 加解密 本地存储
##### useDebounce中 是自定义ref 可以进行数据收集时防抖操作
##### useDefer是一个高阶函数, 因为它将会返回一个新的函数, 通常会用在进行数据列表渲染的时候
##### usePxToVw使用将px转为vw的一个工具函数,十分简单,只需要注意工具中的viewportWidth 和 vite.config.ts 中的一致即可
##### useRouter 使用包含路由跳转和返回的函数, 我们通常使用它进行页面的跳转,因为这样会给页面切换添加动效, 并且 我们在写页面的时候 通常会将页面包含在 components/shell 组件中, 这样我们可以近似的模拟app的缓存页面功能
##### useUrlData 用于获取路由参数的工具函数
##### useUserIdDecode 是用于 userId解密的, 因为app注入到cookie中的userid是加密的,所以在有些情况下 如何想拿到解密后的userid,可以使用此工具函数
#### 原生充值完成之后调用 window.inApplicationIAPRechargeListener(web端提供)
#### web端通过 window.fetchFirstRechargeProduct = Function 来获取首充数据
## 只有每日礼包
// @see: https://cz-git.qbenben.com/zh/guide
/** @type {import('cz-git').UserConfig} */
module.exports = {
ignores: [commit => commit === "init"],
extends: [],
rules: {},
prompt: {
messages: {
type: "选择你要提交的类型 :",
scope: "选择一个提交范围(可选):",
customScope: "请输入自定义的提交范围 :",
subject: "填写简短精炼的变更描述 :\n",
body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n',
breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n',
footerPrefixsSelect: "选择关联issue前缀(可选):",
customFooterPrefixs: "输入自定义issue前缀 :",
footer: "列举关联issue (可选) 例如: #31, #I3244 :\n",
confirmCommit: "是否提交或修改commit ?"
},
types: [
{ value: "feat: 特性", name: "特性: 🚀 新增功能", emoji: "🚀" },
{ value: "fix: 修复", name: "修复: 🧩 修复缺陷", emoji: "🧩" },
{ value: "docs: 文档", name: "文档: 📚 文档变更", emoji: "📚" },
{ value: "style: 格式", name: "格式: 🎨 代码格式(不影响功能,例如空格、分号等格式修正)", emoji: "🎨" },
{ value: "refactor: 重构", name: "重构: ♻️ 代码重构(不包括 bug 修复、功能新增)", emoji: "♻️" },
{ value: "perf: 性能", name: "性能: ⚡️ 性能优化", emoji: "⚡️" },
{ value: "test: 测试", name: "测试: ✅ 添加疏漏测试或已有测试改动", emoji: "✅" },
{ value: "chore: 构建", name: "构建: 📦️ 构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等)", emoji: "📦️" },
{ value: "ci: 集成", name: "集成: 🎡 修改 CI 配置、脚本", emoji: "🎡" },
{ value: "revert: 回退", name: "回退: ⏪️ 回滚 commit", emoji: "⏪️" },
{ value: "build: 打包", name: "打包: 🔨 项目打包发布", emoji: "🔨" }
],
useEmoji: true
}
};
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<link rel="icon" href="/logo.ico">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover"
name="viewport"/>
<meta http-equiv="pragma" content="no-cache" />
<meta http-equiv="cache-control" content="no-cache" />
<meta http-equiv="expires" content="0">
<title>bmiss-充值</title>
</head>
<body>
<!--author: 管忠旭-->
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "bmiss-charge-propaganda",
"private": true,
"version": "1.0.0",
"author": "管忠旭",
"type": "module",
"scripts": {
"dev": "vite --host ",
"build:test": "vue-tsc && vite build --mode test",
"build:standard": "vue-tsc && vite build --mode standard",
"build:production": "vue-tsc && vite build --mode production",
"preview": "vite preview",
"prepare": "husky install",
"lint": "pnpm eslint --config ./.eslintrc.cjs ./src",
"lint-clear": "pnpm lint --config ./.eslintrc.cjs --ext .vue,.ts --cache",
"prettier": "prettier --write --loglevel silent ./src",
"commit": "pnpm prettier && git status && git add -A && git-cz"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-git"
}
},
"dependencies": {
"@vant/use": "^1.6.0",
"animejs": "^3.2.2",
"axios": "^1.6.5",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.10",
"foreign-country-utils": "1.16.0",
"js-base64": "^3.7.5",
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
"md5": "^2.3.0",
"nanoid": "^4.0.2",
"pinia": "^2.1.7",
"reflect-metadata": "^0.1.14",
"swiper": "^11.0.5",
"vant": "^4.8.2",
"vconsole": "^3.15.1",
"vue": "^3.4.10",
"vue-i18n": "^9.14.5",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@types/animejs": "^3.1.12",
"@types/crypto-js": "^4.2.1",
"@types/eslint": "^8.56.2",
"@types/js-cookie": "^3.0.6",
"@types/lodash": "^4.14.202",
"@types/md5": "^2.3.5",
"@types/node": "^18.19.6",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"@vitejs/plugin-vue": "^4.6.2",
"commitizen": "^4.3.0",
"cz-git": "^1.8.0",
"eslint": "^8.56.0",
"eslint-plugin-vue": "^9.20.0",
"husky": "^8.0.3",
"postcss-px-to-viewport": "^1.1.1",
"prettier": "^2.8.8",
"stylus": "^0.59.0",
"terser": "^5.26.0",
"typescript": "^5.3.3",
"unplugin-auto-import": "^0.16.7",
"unplugin-vue-components": "^0.22.12",
"vite": "^4.5.14",
"vite-plugin-eslint": "^1.8.1",
"vue-tsc": "^1.8.27"
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
#app
overflow hidden
.gIn-enter-active,
.gIn-leave-active,
.gOut-enter-active,
.gOut-leave-active{
transition: all .25s linear;
position: absolute;
}
.gIn-leave-to {
transform: translateX(0);
}
.gIn-enter-from
z-index 1
transform translateX(100%)
.gIn-enter-to{
z-index 1
transform: translateX(0);
}
.gOut-leave-from
z-index 1
.gOut-leave-to {
transform: translateX(100%);
z-index 1
}
.gOut-enter-from
transform translateX(0)
.gOut-enter-to{
transform: translateX(0);
}
\ No newline at end of file
/* 在线链接服务仅供平台体验和调试使用,平台不承诺服务的稳定性,企业客户需下载字体包自行发布使用并做好备份。 */
@font-face {
font-family: 'iconfont'; /* Project id 4341786 */
src: url('../font/header.woff2') format('woff2'),
url('../font/header.woff') format('woff'),
url('../font/header.ttf') format('truetype');
}
.iHeader
font-family "iconfont"
font-style normal
\ No newline at end of file
*
font-family -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Liberation Sans", "PingFang SC", "Microsoft YaHei", "Hiragino Sans GB", "Wenquanyi Micro Hei", "WenQuanYi Zen Hei", "ST Heiti", SimHei, SimSun, "WenQuanYi Zen Hei Sharp", sans-serif
/* CSS Document */
html, body, div, span, object, iframe, h1, h2,
h3, h4, h5, h6, p, blockquote, pre, abbr, address, cite, code, del, dfn,
em, img, ins, kbd, q, samp, small, strong, sub, sup, var, b, i, dl, dt, dd,
ol, ul, li, fieldset, form, label, legend, table, caption, tbody,
tfoot, thead, tr, th, td, article, aside, canvas, details, figcaption,
figure, footer, header, hgroup, menu, nav, section, summary, time, mark, button,
audio, video
margin 0
padding 0
border 0
outline none
vertical-align baseline
background transparent
body
color #333
touch-action none
li
list-style none
a
margin 0
padding 0
border 0
vertical-align baseline
background transparent
color #222
text-decoration none
a:hover, a:focus, a:active, a:visited
text-decoration none;
outline-style none; /*FF*/
table
border-collapse collapse
border-spacing 0
input, select
vertical-align middle
/*css为clearfix,清除浮动*/
.clearfix::before,
.clearfix::after
content ""
height 0
line-height 0
display block
visibility hidden
clear both
.clearfix:after
clear both
.removeScrollbar::-webkit-scrollbar
display none
scrollbar-width none
button
outline 0
border 0
cursor pointer
button:hover
opacity: .8
html, body
scroll-behavior smooth
html
overflow-y: scroll
:root
overflow-y auto
/* 检测横屏(最小宽高比) */
@media all and (min-aspect-ratio: 1 / 1)
:root
overflow-x hidden
:root body
position absolute
::-webkit-scrollbar
width 4px
height 4px
border-radius 4px
::-webkit-scrollbar-thumb
background-color #a8a8a8
border-radius 5px
::-webkit-scrollbar-thumb:hover
background-color: #8a8a8a
::-webkit-scrollbar-track
background-color #eee
.one
overflow: hidden
-ms-text-overflow: ellipsis
text-overflow: ellipsis
white-space: nowrap
.pt
padding-top 32px
.pb
padding-bottom 24px
//全局的样式文件,可定义全局的变量、函数、混合等
//多行隐藏默认两行
nLine(line=2){
display -webkit-box;
-webkit-box-orient vertical;
-webkit-line-clamp line;
overflow hidden;
}
borderRadius(radius = 12px){
border-radius radius
}
\ No newline at end of file
<template>
<header :class="{ pt: hasPt }" :style="headerStyle">
<slot name="back">
<i class="iHeader" @click="goBack">&#xe61e;</i>
</slot>
<slot>
<h4 :style="titleStyle" class="one">{{ defaultTitle }}</h4>
</slot>
<slot name="right"></slot>
</header>
</template>
<script lang="ts" setup>
import useRouter from "@/utils/useRouter";
import useTitle from "components/header/hooks/useTitle";
import useHasPt from "components/header/hooks/useHasPt";
import "@/assets/css/header.styl";
interface IProp {
title?: string;
onBack?: () => void;
titleStyle?: Record<string, string>;
headerStyle?: Record<string, string>;
}
const { go } = useRouter();
const { title = "", onBack, titleStyle = {}, headerStyle = {} } = defineProps<IProp>();
const { defaultTitle } = useTitle({ title });
const { hasPt } = useHasPt();
const goBack = () => {
onBack ? onBack() : go(-1);
};
</script>
<style lang="stylus" scoped>
header
position sticky
top 0
left 0
background-color #fff
display flex
align-items center
justify-content center
box-sizing border-box
max-height 70px
flex none
> h4
text-align center
line-height 46px
height 46px
max-width 80%
font-size 16px
> .iHeader
width 30px
height 46px
display flex
align-items center
justify-content flex-start
position absolute
left 16px
</style>
import useCookieStore from "@/store/useCookieStore";
import piniaObj from "@/store/pinia";
const cookieStore = useCookieStore(piniaObj);
export default () => {
const hasPt = ref(cookieStore.cookieData.platform === "app");
return {
hasPt
};
};
import router from "@/router";
/**
* 如何是在微信内则 标题为空,否则标题为pageConfig.ts中的 title 字段的值
*/
interface IParams {
title: string;
}
const isWeiXin = navigator.userAgent.toLowerCase().indexOf("micromessenger") !== -1;
export default ({ title = "" }: IParams) => {
const defaultTitle = ref("");
const initDefaultTitle = () => {
const pageTitle = router.currentRoute.value.meta.title || "";
document.title = title.length > 0 ? title : pageTitle.toString();
if (isWeiXin) {
defaultTitle.value = "";
} else if (pageTitle && typeof pageTitle === "string") {
defaultTitle.value = document.title;
}
};
initDefaultTitle();
onActivated(initDefaultTitle);
return {
defaultTitle
};
};
<template>
<div class="loading">
<div :style="{ transform: `scale(${scale}) rotateY(${deg}deg)` }" :class="{ move }">
<i :style="{ transform: ` rotateY(-${deg}deg)` }" :class="{ left: true, move }"></i>
<i :style="{ transform: ` rotateY(-${deg}deg)` }" :class="{ right: true, move }"></i>
</div>
</div>
</template>
<script lang="ts" setup>
interface IProps {
move?: boolean;
distance: number;
headHeight: number;
}
const props = defineProps<IProps>();
const scale = computed(() => props.distance / props.headHeight);
const deg = computed(() => scale.value * 360);
</script>
<style lang="stylus" scoped>
animaTime = 1s
.loading
width 100%
height 100%
perspective 40px
display flex
flex-direction column
justify-content center
> div
transform-style: preserve-3d
width 30px
height 10px
display flex
justify-content space-between
align-items center
margin 0 auto
backface-visibility visible
&.move
animation move animaTime infinite linear
> i
width 8px
height 8px
border-radius 50%
backface-visibility visible
transform-style: preserve-3d;
animation-direction reverse
&.move
animation move animaTime infinite linear
animation-direction reverse
&.left
background-color #f00
&.right
background-color: #00f
@keyframes move
0%
transform rotateY(0)
100%
transform rotateY(360deg)
</style>
<template>
<van-pull-refresh
:model-value="refreshLoading"
@refresh="emits('onRefresh')"
:head-height="headHeight"
class="shell"
:disabled="refreshDisabled || scrollTop > 0"
>
<!-- 下拉过程提示 -->
<template #pulling="props">
<slot name="iPulling" :props="props">
<Loading :head-height="headHeight" :distance="props.distance" />
</slot>
</template>
<!-- 释放提示 -->
<template #loosing>
<slot name="iLoosing">
<Loading :head-height="headHeight" :distance="headHeight" />
</slot>
</template>
<!-- 加载提示 -->
<template #loading>
<slot name="iLoading">
<Loading :head-height="headHeight" :distance="headHeight" :move="true" />
</slot>
</template>
<van-list
ref="shellRef"
:style="shellStyle"
:loading="loading"
:finished="finished"
:offset="offset"
:loading-text="loadingText"
:finishedText="finishedText"
:errorText="errorText"
:immediateCheck="immediateCheck"
:disabled="disabled"
@load="emits('onLoad')"
class="list removeScrollbar"
>
<slot></slot>
</van-list>
</van-pull-refresh>
</template>
<script lang="ts" setup>
// 参考vant官方文档 https://vant-contrib.gitee.io/vant/#/zh-CN/list
import useScrollTop from "components/shell/hooks/useScrollTop";
import Loading from "components/loading/Loading.vue";
interface IProps {
shellStyle?: Record<string, string | number>;
loading?: boolean;
finished?: boolean;
offset?: number;
loadingText?: string;
finishedText?: string;
errorText?: string;
immediateCheck?: boolean;
disabled?: boolean;
refreshLoading?: boolean;
refreshDisabled?: boolean;
headHeight?: number;
}
interface IEmits {
(eventName: "onLoad"): void;
(eventName: "onRefresh"): void;
}
const emits = defineEmits<IEmits>();
withDefaults(defineProps<IProps>(), {
shellStyle: () => ({}),
loading: false,
finished: true,
offset: 300,
loadingText: "加载中...",
finishedText: "",
errorText: "",
immediateCheck: true,
disabled: false,
headHeight: 100,
refreshDisabled: true
});
const { shellRef, scrollTop } = useScrollTop();
</script>
<style lang="stylus" scoped>
.shell
width 100%
height 100%
.list
width 100%
height 100%
overflow-y auto
display flex
flex-direction column
</style>
import { ListInstance } from "vant";
export default () => {
const shellRef = ref<ListInstance>();
const scrollTop = ref(0);
onActivated(() => {
const ele = shellRef.value;
if (ele) {
ele.$el.scrollTo({
left: 0,
top: scrollTop.value
});
}
});
const onScroll = (ele: HTMLElement) => {
scrollTop.value = ele.scrollTop;
};
const bindScroll = () => {
const ele = shellRef.value?.$el;
if (ele) {
ele.addEventListener("scroll", onScroll.bind(null, ele));
}
};
const unBindScroll = () => {
const ele = shellRef.value?.$el;
if (ele) {
ele.removeEventListener("scroll", onScroll.bind(null, ele));
}
};
onMounted(bindScroll);
onUnmounted(unBindScroll);
return {
shellRef,
scrollTop
};
};
/**
* 这是一个图片展示的指令,用于在ios14及以下展示png类型的图片,ios14以上使用webp类型的图片, 安卓一律使用webp类型的图片
*/
import { Directive, DirectiveBinding } from "vue";
import handleApp from "@/utils/HandleApp";
import { getLocale } from "@/locales/locale";
const ENV = import.meta.env;
interface OnError {
(): void;
}
export interface ShowImageDirective {
src: string;
onError?: OnError;
isBack?: boolean;
}
interface HandleImg {
ele: HTMLImageElement;
src: string;
onError: OnError;
}
interface HandleBackImg {
ele: HTMLDivElement;
src: string;
onError: OnError;
}
let baseDir = `${location.origin}${location.pathname}`;
let errorPath = "";
if (ENV.MODE === "development") {
baseDir += "public/";
}
baseDir += "images";
errorPath = `${baseDir}/error/error.png`;
const lang = getLocale();
// 正常图片处理
const handleImg = ({ ele, src, onError }: HandleImg) => {
const [dirPath, fileName] = src.split("/");
if(lang == "en"){
ele.src = `${baseDir}/${dirPath}/enpng/${fileName}.png`;
}else{
if (handleApp.agent === "ios" && handleApp.iosVersion.minorVersion < 14) {
ele.src = `${baseDir}/${dirPath}/png/${fileName}.png`;
} else {
ele.src = `${baseDir}/${dirPath}/webp/${fileName}.webp`;
}
}
ele.onerror = onError;
};
// 背景图片处理
const handleBackImg = ({ ele, src, onError }: HandleBackImg) => {
let image: HTMLImageElement | null = new Image();
image.onload = () => {
image = null;
};
image.onerror = () => {
onError();
image = null;
};
const [dirPath, fileName] = src.split("/");
if(lang == "en"){
ele.style.backgroundImage = `url(${baseDir}/${dirPath}/enpng/${fileName}.png)`;
image.src = `${baseDir}/${dirPath}/enpng/${fileName}.png`;
}else{
if (handleApp.agent === "ios" && handleApp.iosVersion.minorVersion < 14) {
// ios版本小于14,则使用png类型的图片
ele.style.backgroundImage = `url(${baseDir}/${dirPath}/png/${fileName}.png)`;
image.src = `${baseDir}/${dirPath}/png/${fileName}.png`;
} else {
ele.style.backgroundImage = `url(${baseDir}/${dirPath}/webp/${fileName}.webp)`;
image.src = `${baseDir}/${dirPath}/webp/${fileName}.webp`;
}
}
};
const render = (ele: HTMLImageElement, binding: DirectiveBinding<ShowImageDirective | string>, vNode: globalThis.VNode) => {
let src: string;
let onError: OnError = () => {
ele.src = errorPath;
};
if (vNode.props?.onError) {
onError = vNode.props.onError;
}
if (typeof binding.value === "string") {
// 默认是img标签, 进行src处理和错误函数绑定
src = binding.value;
handleImg({ ele, src, onError });
} else {
src = binding.value.src;
binding.value.onError && (onError = binding.value.onError);
if (binding.value.isBack) {
// 默认是div 标签 是背景图片处理
onError = () => {
ele.style.backgroundImage = `url(${errorPath})`;
};
if (vNode.props?.onError) {
onError = vNode.props.onError;
}
handleBackImg({ ele, src, onError });
} else {
// img标签 进行src处理和错误函数绑定
handleImg({ ele, src, onError });
}
}
};
const vShowImg: Directive = {
beforeMount(ele, binding: DirectiveBinding<ShowImageDirective | string>, vNode) {
const value = binding.value;
if (isProxy(value)) watch(() => value, render.bind(null, ele, binding, vNode), { deep: true });
render(ele, binding, vNode);
}
};
export default vShowImg;
import { plainToInstance, ClassConstructor } from "class-transformer";
import { validate, ValidationError, ValidatorOptions } from "class-validator";
const defaultValidatorOptions: ValidatorOptions = {
skipMissingProperties: false,
stopAtFirstError: true,
forbidUnknownValues: true
};
// 基础的数据校验类型, 包含了指定的数据校验、平面对象转类对象
export default abstract class BaseEntities {
[key: string]: unknown;
/**
* 验证对象是否符合数据格式
* @param config
*/
public async validator(config: ValidatorOptions = {}) {
const errors = await validate(this, { ...defaultValidatorOptions, ...config });
return BaseEntities.findError(errors);
}
/**
* 平面对象转换类对象
* @param Obj
* @param planObj
*/
public static transform<T>(Obj: ClassConstructor<T>, planObj: object): T {
if (planObj instanceof Obj) return planObj;
return plainToInstance(Obj, planObj);
}
private static findError(errors: ValidationError[], resultArr: string[] = []): string[] {
const arr = errors.map(item => item.constraints);
arr.forEach(item => {
if (item) resultArr.push(...Object.values(item));
});
errors.forEach(item => {
if (item && item.children) {
BaseEntities.findError(item.children, resultArr);
}
});
return resultArr;
}
}
import { IsInt } from "class-validator";
import BaseEntities from "entities/BaseEntities";
// 分页请求需要继承类,已自己继承了BaseEntities类
export default abstract class BasePageRequest extends BaseEntities {
@IsInt({ message: "page 必须为number类型" })
page = 1;
@IsInt({ message: "page_size 必须为number类型" })
page_size = 10;
}
import BaseEntities from "entities/BaseEntities";
import { IsInt, IsString } from "class-validator";
import useErrorMessage from "@/utils/useErrorMessage";
export default class FirstGift extends BaseEntities {
@IsString({ message: useErrorMessage("title", "string") })
title = "";
@IsString({ message: useErrorMessage("title", "string") })
src = "";
@IsString({ message: useErrorMessage("productName", "string") })
productName = "";
@IsString({ message: useErrorMessage("productLocaleSymbol", "string") })
productLocaleSymbol = "¥";
@IsInt({ message: useErrorMessage("productPrice", "int") })
productPrice = 0;
@IsString({ message: useErrorMessage("productIdentifier", "string") })
productIdentifier = "";
@IsInt({ message: useErrorMessage("ratio", "int") })
ratio = 0;
constructor(title?: string, src?: string, ratio?: number) {
super();
if (title) this.title = title;
if (src) this.src = src;
if (ratio) this.ratio = ratio;
}
}
import { ToastOptions, ToastType } from "vant/lib/toast/types";
import { allowMultipleToast, showDialog, showFailToast, showLoadingToast, showSuccessToast, showToast, ToastWrapperInstance } from "vant";
import { DialogOptions } from "vant/lib/dialog/types";
import { DialogAction } from "vant/es/dialog/types";
const defaultOption: ToastOptions = {
duration: 0,
forbidClick: true,
message: "加载中...",
overlay: true
};
allowMultipleToast();
export default class Tips {
// 加载相关
static showLoading(options: string | ToastOptions = {}): ToastWrapperInstance {
let config: ToastOptions = { ...defaultOption };
if (typeof options === "string") {
config.message = options;
} else {
config = {
...config,
...options
};
}
return showLoadingToast(config);
}
// 数据校验出错弹窗
static showDataError(message: string, options: DialogOptions = {}): Promise<DialogAction | undefined> {
return showDialog({
title: "提示",
message,
...options
});
}
// 轻提醒
static showToast(options: string | ToastOptions = {}, type: ToastType = "success"): ToastWrapperInstance {
switch (type) {
case "success":
return showSuccessToast(options);
case "fail":
return showFailToast(options);
case "html":
case "text":
case "loading":
return showToast(options);
}
}
}
type BaseType = symbol | string | number | null | unknown | boolean;
interface BaseResponse<T> {
data: T;
msg: string;
code: number;
}
interface DataObj {
[key: string]: BaseType | Array<BaseType> | Array<Record<string, BaseType>>;
}
interface Secret {
key: string;
iv: string;
}
export default {
tag: "首充禮包",
title1: "初级",
title2: "精選",
title3: "豪華",
title4: "尊享",
title5: "璀璨",
text1: "搶購",
text2: "原價",
text3: "已搶購",
appletext: "本活動與蘋果公司無關,解釋權歸平台所有",
gooletext: "本活動與谷歌公司無關,解釋權歸平臺所有",
guizhe: "首充禮包規則",
guizhe1: "1.此活動僅針對未充值過的用戶;",
guizhe2: "2.未充值的用戶只允許購買其中一檔禮包,購買後其他檔位的禮包無法購買;",
guizhe3: "3.平臺禁止一切作弊、外掛等違反平臺規則的行為,一經發現,取消獎勵,並給予相應懲罰;",
guizhe4: "4.平臺有權基於運營情況,對禮包的獎勵進行調整;",
guizhe5: "5.本次活動與Apple Inc.或Google LLC無關。",
tishi: "不符合購買要求,只有首次充值才可購買哦~",
};
export default {
tag: "First Recharge",
title1: "Basic",
title2: "Selected",
title3: "Luxury",
title4: "Premium",
title5: "Brilliant",
text1: "panic buying",
text2: "original price",
text3: "purchased",
appletext: "This activity is not related to Apple Inc., and the interpretation rights belong to the platform",
gooletext: "This activity is not related to Google, and the interpretation rights belong to the platform",
guizhe: "First Charge Gift Pack Rules",
guizhe1: "1.This activity is only for users who have not recharged before;",
guizhe2: "2.Users who have not recharged are only allowed to purchase one of the gift packages. After purchasing, other gift packages cannot be purchased;",
guizhe3: "3.The platform prohibits all cheating, cheating, and other violations of platform rules. Once discovered, rewards will be cancelled and corresponding punishments will be imposed;",
guizhe4: "4.The platform has the right to adjust the rewards of gift packages based on operational conditions;",
guizhe5: "5.This event and Apple Inc. or Google LLC irrelevant.",
tishi: "First recharge is required for purchase",
};
\ No newline at end of file
import cn from "./cn";
import en from "./en";
export default {
"zh-TW": cn,
en
};
\ No newline at end of file
import Cookie from "js-cookie"; // 假设你使用了js-cookie
export const getLocale = () => {
let savedLanguage = Cookie.get("lang") || "en";
savedLanguage = savedLanguage.includes("zh") ? "zh-TW" : "en";
return savedLanguage;
};
\ No newline at end of file
import "reflect-metadata";
import { createApp } from "vue";
import App from "views/app/App.vue";
import pinia from "@/store/pinia";
import router from "@/router";
import { Lazyload } from "vant";
import vShowImg from "@/directives/showImage";
import messages from "@/locales/index";
import { createI18n } from "vue-i18n";
import { getLocale } from "./locales/locale";
const app = createApp(App);
app.use(pinia);
app.directive("showImage", vShowImg);
app.use(Lazyload, {
lazyComponent: true
});
const lang = getLocale();
localStorage.setItem("lang",lang);
const i18n = createI18n({
legacy: false,
locale: lang,
messages
});
app.use(i18n);
// 使用 i18n 插件
app.use(createI18n);
app.use(router);
app.mount("#app");
import { Http } from "foreign-country-utils";
import requestError from "@/request/requestError";
import encodeHeader from "@/request/usePostCommonParam";
import Secret_key from "@/secret_key/secret_key";
import { Base64 } from "js-base64";
import { decodeData, encodeData } from "@/request/useRequestEncryption";
import { AxiosRequestConfig, InternalAxiosRequestConfig } from "axios";
import usePrint from "@/request/usePrint";
const ENV = import.meta.env;
const baseUrl = ENV.VITE_BASE_URL;
const timeout = 30 * 1000;
const secretLength = Object.keys(Secret_key).length;
// 以下是基础请求,可自行添加加密方式
export const http = new Http(baseUrl, timeout, {}, [
{
request: {
before(config: InternalAxiosRequestConfig) {
const key = `secret_key_${1 + Math.floor(Math.random() * secretLength)}`;
const secret = Secret_key[key];
let tempParams: unknown = {};
const { method, url = "", params, data } = config;
if (method === "get") {
ENV.MODE !== "production" && usePrint(params || {}, method, url);
if (params) {
tempParams = params;
const sign_data = encodeURIComponent(
Base64.encode(encodeData(typeof params === "string" ? params : JSON.stringify(params), secret))
);
config.params = { sign_data: sign_data };
}
}
if (method === "post") {
ENV.MODE !== "production" && usePrint(data || {}, method, url);
if (data) {
tempParams = data;
const sign_data = encodeURIComponent(
Base64.encode(encodeData(typeof data === "string" ? data : JSON.stringify(data), secret))
);
config.data = { sign_data };
}
}
const enHeader = encodeHeader(key, "", tempParams);
for (const stringsKey in enHeader) {
config.headers.set(stringsKey, enHeader[stringsKey]);
}
return config;
}
},
response: {
success(result) {
const data = result.data;
if (data && typeof data === "object" && "encrypted_data" in data) {
const secret = Secret_key[result.config.headers.sign_name];
if (secret) {
if (typeof data.encrypted_data === "string" && data.encrypted_data.length === 0) {
// @ts-ignore
result.data.data = null;
} else {
// @ts-ignore
result.data.data = JSON.parse(decodeURIComponent(Base64.decode(decodeData(data.encrypted_data as string, secret))));
}
} else {
throw "解密失败";
}
}
return result;
},
error(err) {
// 请求错误提醒
requestError(err.message);
return Promise.reject(err);
}
}
}
]);
export const get = <T, D = DataObj>(url: string, params?: D, config?: AxiosRequestConfig<D>): Promise<BaseResponse<T>> =>
http.get(url, params, config);
export const post = <T, D = DataObj>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<BaseResponse<T>> =>
http.post<BaseResponse<T>, D>(url, data, config);
export const upload = <T>(url: string, data: FormData, config?: AxiosRequestConfig): Promise<BaseResponse<T>> =>
http.upload<BaseResponse<T>>(url, data, config);
export const remove = <T, D = DataObj>(url: string, params?: D, config?: AxiosRequestConfig<D>) =>
http.delete<BaseResponse<T>, D>(url, params, config);
export const put = <T, D = DataObj>(url: string, params?: D, config?: AxiosRequestConfig<D>) => http.put<BaseResponse<T>, D>(url, params, config);
import { closeToast } from "vant";
import { RequestError } from "@/types/RequestError";
import Tips from "entities/Tips";
const errorList: RequestError[] = [
[(msg: string) => msg.includes("Network Error"), () => Tips.showDataError("网络错误")],
[(msg: string) => msg.includes("timeout"), () => Tips.showDataError("30秒内后端未返回数据,请求超时!")],
[(msg: string) => msg.includes("500"), msg => Tips.showDataError(msg || "后端出错")]
];
export default (msg: string) => {
const has = errorList.find(item => item[0](msg));
closeToast(true);
if (has) {
has[1](msg);
} else {
Tips.showDataError(msg);
}
};
export default () => {
const lowercase = [...Array(26)].map((item, index) => index + 97).map(item => String.fromCharCode(item));
const numCase = [...Array(10)].map((item, index) => index + 48).map(item => String.fromCharCode(item));
const uppercase = [...Array(26)].map((item, index) => index + 65).map(item => String.fromCharCode(item));
return [...numCase, ...uppercase, ...lowercase];
};
// 添加头公参函数
import piniaObj from "@/store/pinia";
import useRequestData from "@/store/useRequestData";
import useCookieStore from "@/store/useCookieStore";
import md5 from "md5";
import useChat from "@/request/useChat";
import { random } from "lodash";
const requestStore = useRequestData(piniaObj);
const cookieStore = useCookieStore(piniaObj);
const chatList = useChat();
const lang = localStorage.getItem("lang") || "en";
export default (sign_name: string, customSignName = "", params: unknown = {}) => {
const data: Record<string, string> = {
device: "wap",
source: cookieStore.cookieData.source || "xinxiuweb",
request_number: Math.random().toString(),
timestamp: Math.floor(Date.now() / 1000).toString(),
appversion: "1.0.0",
token: requestStore.requestData.token,
lang:lang
};
if (typeof params === "object") {
// 这里偷懒直接合并了, 正常应该去除公参的key ,不过如果 body的公参的key冲突了 其实也是不合理的, 我觉得这个应该是后端设计的问题
Object.assign(data, params);
}
let signature = "";
const keys = Object.keys(data);
keys.sort();
keys.forEach(item => {
if (data[item] !== undefined) {
signature += data[item];
}
});
signature += "asdasgfdwqew";
signature = md5(md5(signature));
for (let i = 0; i < 11; i++) {
if (i < 5) {
signature = chatList[random(0, 61)] + signature;
} else {
signature += chatList[random(0, 61)];
}
}
data.signature = signature;
if (sign_name) {
data.sign_name = sign_name;
}
if (customSignName.length > 0) {
data.sign_name = customSignName;
}
return data;
};
import dayjs from "dayjs";
const proxyColors = () => {
let index = 0;
const consoleColors: string[] = ["#409eff", "#67c23a", "#e6a23c", "#f56c6c"];
return {
get() {
return consoleColors[index++ % consoleColors.length];
}
};
};
const colors = proxyColors();
export default <T>(params: T, method: "get" | "post", url: string) => {
if (typeof params === "object") {
console.log(
"%c %s %c %s %c %s %c %s",
`color:${colors.get()};`,
`请求时间:${dayjs().format("YYYY-MM-DD HH:mm:ss")},`,
`color:${colors.get()};`,
`请求方法:${method},`,
`color:${colors.get()};`,
`请求地址:${url},`,
`color:${colors.get()};`,
`请求参数:${JSON.stringify(params)};`
);
}
};
import { AES, enc, mode, pad } from "crypto-js";
const { CBC } = mode;
const { Pkcs7 } = pad;
const { parse } = enc.Utf8;
export const decodeData = (str: string, secret: Secret) => {
return AES.decrypt(str, parse(secret.key), {
iv: enc.Utf8.parse(secret.iv),
mode: CBC,
padding: Pkcs7
}).toString(enc.Utf8);
};
export const encodeData = (data: string, secret: Secret) =>
AES.encrypt(data, parse(secret.key), {
iv: enc.Utf8.parse(secret.iv),
mode: CBC,
padding: Pkcs7
}).toString();
import jsonObj from "../../package.json";
const BASEURL = `${jsonObj.name}`;
export default BASEURL;
import { createRouter, createWebHashHistory } from "vue-router";
import BASEURL from "@/router/baseUrl";
import useCreateRoutes from "@/router/useCreateRoutes";
const router = createRouter({
history: createWebHashHistory(BASEURL),
routes: useCreateRoutes()
});
export default router;
import { RouteMeta, RouteRecordRaw } from "vue-router";
import useFindTier from "@/router/useFindTier";
export default () => {
const components = import.meta.glob("../views/**/*.vue");
const routes: RouteRecordRaw[] = [];
const obj: Record<number, { path: string; mate: RouteMeta }[]> = {};
useFindTier().map(([path, mate]) => {
const key = path.replace("/pageConfig.ts", "").split("/").length;
if (!obj[key]) {
obj[key] = [];
}
obj[key].push({
path,
mate
});
});
const tempRouter: RouteRecordRaw[] = [];
const createR = (arr: { path: string; mate: RouteMeta }[], tier: number) => {
arr.forEach(item => {
const temp = item.path.replace("/pageConfig.ts", "");
const last = temp.split("/")?.pop() || "";
const name = `${last.slice(0, 1).toUpperCase()}${last.slice(1)}`;
const lastPath = `/${name}.vue`;
let path = item.path.replace("../views", "").replace("/pageConfig.ts", "");
path = `/${path.split("/").filter(Boolean).join("/")}`;
const route: RouteRecordRaw = {
path,
name,
component: components[`${temp}${lastPath}`],
meta: item.mate,
children: [],
redirect: undefined
};
if (tier === 0) {
routes.push(route);
} else {
const has = tempRouter.filter(i => temp.includes(i.path));
const redirect = route.path;
route.path = route.path.split("/").at(-1) || "";
const obj: RouteRecordRaw = has.length === 1 ? has[0] : (has.at(-1) as RouteRecordRaw);
obj.children?.push(route);
if (item.mate.redirect) {
obj.redirect = redirect;
}
}
tempRouter.push(route);
});
};
let i = 0;
for (const objKey in obj) {
createR(obj[objKey], i);
i++;
}
routes.push({
path: "/:pathMatch(.*)*",
redirect: "/index"
});
return routes;
};
const components = import.meta.glob("../views/**/*.vue");
// 查询vue文件
export default (key: string) => components[key];
import { RouteMeta } from "vue-router";
export default () => {
const temp = import.meta.glob<RouteMeta>("../views/**/pageConfig.ts", {
eager: true,
import: "default"
});
return Object.entries<RouteMeta>(temp);
};
// 秘钥本,如果后端更新前端也需更新
const secrets: Record<string, Secret> = {
secret_key_1: {
key: "k0f3JfxEWd3HC7pXQU8tmSkDheUXibmz",
iv: "Fu2wU73MBcsEZWJk"
},
secret_key_2: {
key: "NdEHDuAZGCQV6C0oaURvBJJWA4z2QHyG",
iv: "w0j9K4bswcGJKtj5"
},
secret_key_3: {
key: "lBsn3FrYN9hdoFqMdqqNV1dlpGutkcXk",
iv: "NRHqJYTNKxH8Z9fU"
},
secret_key_4: {
key: "eVdEc44rke6P6rfn9SgEWGPNqkcipWN7",
iv: "l5HNU6Q0bdc2piB3"
},
secret_key_5: {
key: "53BSHwE29I1u4e5cJDMZGHUdi0A7L4E5",
iv: "zUsjrk58p2RahP08"
},
secret_key_6: {
key: "vgizOWReMzVJA6LEsb9N36LEzPqcFdeO",
iv: "e52YLbnvVv4HpGu7"
}
};
export default secrets;
import { createPinia } from "pinia";
const piniaObj = createPinia();
export default piniaObj;
import { defineStore } from "pinia";
import { reactive } from "vue";
interface AnimationData {
isBack: boolean;
trigger: boolean;
isFirst: boolean;
}
export default defineStore("useAnimationStore", () => {
const animationData = reactive<AnimationData>({
isBack: false,
trigger: false,
isFirst: true
});
const handleAnimationData = (param: Partial<AnimationData>) => {
Object.assign(animationData, param);
};
return {
animationData,
handleAnimationData
};
});
import { defineStore } from "pinia";
const useCacheRouter = defineStore("useCacheRouter", () => {
const cacheRouter = reactive<string[]>([]);
/**
* 处理缓存路由
* @param routerName 要处理的路由名字
* @param isDelete 是否要从缓存列表中删除要处理的路由名字
*/
const handleRouter = (routerName: string, isDelete = false) => {
if (isDelete) {
deleteRouter(routerName);
} else {
addRouter(routerName);
}
};
const addRouter = (routerName: string) => {
const routerIndex = cacheRouter.findIndex(item => item === routerName);
if (routerIndex === -1) cacheRouter.push(routerName);
};
const deleteRouter = (routerName: string) => {
const deleteRouterIndex = cacheRouter.findIndex(item => item === routerName);
if (deleteRouterIndex !== -1) cacheRouter.splice(deleteRouterIndex, 1);
};
return {
cacheRouter,
handleRouter
};
});
export default useCacheRouter;
import { defineStore } from "pinia";
import useCookie from "@/utils/useCookie";
import { reactive } from "vue";
import AppCookieData from "@/types/AppCookieData";
import Cookie from "js-cookie";
const useCookieStore = defineStore("useCookieStore", () => {
const cookieData = reactive<AppCookieData>(useCookie());
const handleCookieData = (obj: Partial<AppCookieData>) => {
Object.assign(cookieData, obj);
type Key = keyof AppCookieData;
let k: Key;
for (k in obj) {
Cookie.set(k, obj[k]?.toString() || "");
}
};
return {
cookieData,
handleCookieData
};
});
export default useCookieStore;
import { defineStore } from "pinia";
import { reactive } from "vue";
const useRequestData = defineStore("useRequestData", () => {
const requestData = reactive<Record<string, string>>({});
const handle = (key: string, value: string | null) => {
if (value) {
requestData[key] = value;
} else {
delete requestData[key];
}
};
return {
handle,
requestData
};
});
export default useRequestData;
import { defineStore } from "pinia";
import { reactive } from "vue";
import useUrlData from "@/utils/useUrlData";
import { useStorage } from "foreign-country-utils";
import { encode } from "@/utils/useCrypto";
const { set } = useStorage();
type UrlData = Record<string, string>;
const useUrlParamStore = defineStore("useUrlParamStore", () => {
const urlData = reactive<UrlData>(useUrlData());
function handleUrlData(key: keyof UrlData | UrlData, value?: UrlData[keyof UrlData] | null) {
if (typeof key === "string") {
if (value) {
urlData[key] = value;
} else {
delete urlData[key];
}
} else {
Object.assign(urlData, key);
}
const temp: Record<string, string> = {};
for (const k in urlData) {
temp[encode(k)] = encode(urlData[k]);
}
set(encode("useUrlParamStore"), temp);
}
handleUrlData(urlData);
return {
urlData,
handleUrlData
};
});
export default useUrlParamStore;
import Platform from "@/types/Platform";
import InLive from "@/types/InLive";
/**
* isOnLiveRoom = true 并且roomIdentifier有值,说明当前用户是已观众身份进入直播间
* isOnLiveRoom = false 并且roomIdentifier为空,说明当前用户是已主播身份进入直播间
* aes加密(加密方式CBC)
* count = 128
* key = Hy7pawh7PNkiNttFRm35iGPrXnhhprtB
* iv = JhyETrRHXKCdayck
*/
interface AppCookieData {
platform?: Platform;
userid?: string; // aes加密后再base64编码后的字符串
app_token?: string; // 用户token
source?: string; // app包名,包含"com.bang.live.app"则是Bei系列包或者模块
isOnLiveRoom?: InLive; // 是否在直播间内 1在 0不在
roomIdentifier?: string; //观众所在直播间房间号,多个房间以,分割
token?: string; // 自己手动添加的token
}
export default AppCookieData;
type InLive = "0" | "1";
export default InLive;
type Platform = "app";
export default Platform;
export type RequestError = [(msg: string) => boolean, (msg: string) => Promise<unknown>];
type ValueOf<T> = T[keyof T];
export default ValueOf;
import { parseInt } from "lodash";
interface ShareParams {
url: string; // 分享链接
img?: string; // 缩略图链接
title: string; // 展⽰的标题
dec: string; // 标题下的描述
}
const ua = navigator.userAgent;
class HandleApp {
static get iosVersion(): { patchVersion: number; majorVersion: number; minorVersion: number } {
HandleApp._iosVersion.majorVersion === -1 && HandleApp.getAgent();
return HandleApp._iosVersion;
}
static get agent(): string {
HandleApp._agent.length === 0 && HandleApp.getAgent();
return HandleApp._agent;
}
// 当前是安卓还是ios
private static _agent = "";
private static _iosVersion = {
majorVersion: -1,
minorVersion: -1,
patchVersion: -1
};
// 当前的环境
private static getAgent() {
const agent = ua.toLowerCase();
HandleApp._agent = agent.indexOf("android") > -1 || agent.indexOf("adr") > -1 ? "android" : "ios";
if (HandleApp.agent === "ios") HandleApp.getIOSVersion();
}
public static getIOSVersion() {
const iOSVersionMatch = ua.match(/OS (\d+)_(\d+)_?(\d+)?/);
if (iOSVersionMatch) {
// 提取iOS主要版本号、次要版本号和补丁版本号
const majorVersion = parseInt(iOSVersionMatch[1], 10);
const minorVersion = parseInt(iOSVersionMatch[2], 10);
const patchVersion = parseInt(iOSVersionMatch[3] || "0", 10);
HandleApp._iosVersion = {
majorVersion,
minorVersion,
patchVersion
};
}
}
// 关闭webview
public static closeWebview() {
HandleApp.agent === "ios" ? window.webkit.messageHandlers.closeWeb.postMessage({ url: "" }) : window.liveapp.closeWeb();
}
// 分享页面
public static share(param: ShareParams) {
HandleApp.agent === "ios"
? window.webkit.messageHandlers.openShareTip.postMessage(param)
: window.liveapp.openShareTip(JSON.stringify(param));
}
// 进入个人中心
public static toPerson(data: { userid: number | string }) {
HandleApp.agent === "ios" ? window.webkit.messageHandlers.toPerson.postMessage(data) : window.liveapp.toPerson(JSON.stringify(data));
}
// 进入直播间
public static toLiveRoom(param: { userid: number; avatar: string }) {
HandleApp.agent === "ios" ? window.webkit.messageHandlers.toLiveroom.postMessage(param) : window.liveapp.toLiveroom(JSON.stringify(param));
}
// 充值
public static toRecharge(inLive = false) {
HandleApp.agent === "ios" ? HandleApp.iosPay(inLive) : HandleApp.androidPay(inLive);
}
// 安卓充值
public static androidPay(inLive: boolean) {
// 全屏
const data = {
app_open: "appjump",
url: "myWallet"
};
// 半屏
if (inLive) {
data.url = "roomRecharge";
}
window.liveapp.actionAsPhp(JSON.stringify(data));
}
// 苹果支付
private static iosPay(inLive: boolean) {
const str = { className: "MineWalletViewController" }; // 不在直播间
if (inLive) {
//在直播间
str.className = "LiveRechargeViewController";
}
const obj = {
app_open: "appjump",
url: JSON.stringify(str)
};
window.webkit.messageHandlers.actionAsPhp.postMessage(obj); //苹果
}
// 获取状态栏高度(由于原生原因,目前无法获取,默认顶部padding 35px)
public static getStatusBarHeight() {
if (HandleApp.agent === "ios") {
return window.webkit.messageHandlers.getStatusBarHeight.postMessage();
} else {
return window.liveapp.getStatusBarHeight();
}
}
//跳转广场
public static toSquare(param: { index: number }) {
HandleApp.agent === "ios"
? window.webkit.messageHandlers.selecetTabBarItem.postMessage(param)
: window.liveapp.selecetTabBarItem(JSON.stringify(param));
}
// 获取首充礼包数据
public static fetchFirstRechargeProduct() {
HandleApp.agent === "ios"
? window.webkit.messageHandlers.fetchFirstRechargeProduct.postMessage(true)
: window.liveapp.fetchFirstRechargeProduct();
}
// 购买首充礼包
public static buyFirstRechargeProduct(productIdentifier: string) {
HandleApp.agent === "ios"
? window.webkit.messageHandlers.buyFirstRechargeProduct.postMessage({ productIdentifier })
: window.liveapp.buyFirstRechargeProduct(productIdentifier);
}
}
export default HandleApp;
import Cookies from "js-cookie";
import { encode } from "js-base64";
const env = import.meta.env;
export default () => {
const obj: Record<string, string> = Cookies.get();
if (obj.userid) {
obj.userid = encode(obj.userid);
}
if (env.MODE === "development") {
// 海外测试token
// obj.userid = "1387c47aea6bdc948ba9756a4b8cf788";
obj.userid = "378750e045b19c063953f28202c47ffc";
}
return obj;
};
import { enc, AES, mode, pad } from "crypto-js";
const { Pkcs7 } = pad;
const { ECB } = mode;
const key = "cqfjkjgs"; // 重庆凡娇科技公司
const config = {
mode: ECB,
padding: Pkcs7
};
export const encode = (value: string) => {
return AES.encrypt(value, enc.Utf8.parse(key), config).toString();
};
export const decode = (value: string) => {
return AES.decrypt(value, enc.Utf8.parse(key), config).toString(enc.Utf8);
};
import { customRef } from "vue";
export default <T>(value: T, wait = 1000) => {
let timer = 0;
return customRef((track, trigger) => {
return {
get() {
track();
return value;
},
set(val: T) {
clearTimeout(timer);
timer = window.setTimeout(() => {
clearTimeout(timer);
value = val;
trigger();
}, wait);
}
};
});
};
import { Ref } from "vue";
export default <T>(arr: T[] | string) => {
let currentTrack: Ref<number> | null = ref(0);
let timer = 0;
let total = arr.length;
if (isProxy(arr)) {
watch(
() => arr,
() => {
total = arr.length;
cancelAnimationFrame(timer);
draw(total);
},
{
deep: true
}
);
}
const draw = (total: number) => {
if (currentTrack) {
if (currentTrack.value > total) {
cancelAnimationFrame(timer);
return;
}
currentTrack.value++;
}
timer = requestAnimationFrame(draw.bind(null, total));
};
onUnmounted(() => {
currentTrack = null;
});
return (current: number) => {
if (timer === 0) draw(total);
if (currentTrack) {
return currentTrack.value > current;
}
return false;
};
};
export default (key: string, type: string) => `${key} 必须为 ${type} 类型`;
// px 转 vw
// 重要: UI设计稿的宽度 需要和vite.config.ts 中的一致!!!
const viewportWidth = 375;
export default (size: number) => `${(size / viewportWidth) * 100}vw`;
import router from "@/router";
import { RouteLocationRaw } from "vue-router";
import useAnimationStore from "@/store/useAnimationStore";
import { throttle } from "lodash";
import useCacheRouter from "@/store/useCacheRouter";
import piniaObj from "@/store/pinia";
const wait = 250;
const cacheRouterStore = useCacheRouter(piniaObj);
export default () => {
const animationStore = useAnimationStore();
const push = throttle(async (to: RouteLocationRaw) => {
const routerName = router.currentRoute.value.name;
if (routerName) cacheRouterStore.handleRouter(routerName.toString());
animationStore.handleAnimationData({
isBack: false,
trigger: true
});
await router.push(to);
reduction();
}, wait);
const go = throttle(async (delta: number) => {
const routerName = router.currentRoute.value.name;
if (routerName) cacheRouterStore.handleRouter(routerName.toString(), true);
animationStore.handleAnimationData({
isBack: true,
trigger: true
});
router.go(delta);
reduction();
}, wait);
// 还原
const reduction = () => {
setTimeout(() => {
animationStore.handleAnimationData({
isBack: false,
trigger: false
});
}, wait);
};
return {
push,
go
};
};
import { useStorage } from "foreign-country-utils";
import { decode, encode } from "@/utils/useCrypto";
import { cloneDeep } from "lodash";
const { get } = useStorage();
// 获取url地址参数
const reg = /^http[^?]*\?/;
const itemReg = /[^?]*=/;
/**
* 特别说明:
* 1、如果是这样的地址:http://192.168.4.143:5173/#/index?name=5464&backUrl=http://121345465.com?name=999&age=99966656
* 得到的结果是
* {
* "name": "5464",
* "backUrl": "http://121345465.com?name=999",
* "age": "99966656"
* }, 所以如果url地址上需要添加;链接,并且链接上需要带参,就需要在跳转当前项目之前 进行url地址包装
* 2、该方法是从本地的session 里面取值, 并进行了参数的解密
*/
export default (readOld = true) => {
let storage: Record<string, string> | string = {};
readOld && (storage = get<Record<string, string>>(encode("useUrlParamStore"), true) || {});
if (typeof storage === "string") {
storage = {};
} else {
for (const key in storage) {
storage[decode(key)] = decode(storage[key]);
delete storage[key];
}
}
const result: Record<string, string> = cloneDeep(storage);
const href = decodeURIComponent(location.href);
if (!href.includes("?")) return result;
const paramsStr = decodeURIComponent(location.href).replace(reg, "");
if (paramsStr.length > 0 && !paramsStr.includes("&")) {
addValue(result, paramsStr);
} else if (paramsStr.includes("&")) {
paramsStr.split("&").forEach(item => addValue(result, item));
}
return result;
};
const addValue = (obj: Record<string, string>, str: string) => {
const value = str.replace(itemReg, "");
const key = str.split(value)[0].split("=")[0];
obj[key] = value;
};
import { enc, AES, mode, pad } from "crypto-js";
const key = enc.Utf8.parse("Hy7pawh7PNkiNttFRm35iGPrXnhhprtB");
const iv = enc.Utf8.parse("JhyETrRHXKCdayck");
/**
*
* 进行userid解密
* app注入的userid是加密后的,接口需要解密后的userid,所以可使用此方法进行解密
* 注意 参数必须是app注入的, cookieStore中的userid是base64加密过一次的,所以使用前需要用base64解密
* @param userId
*/
export default (userId: string) => {
const aesResult = AES.decrypt(userId, key, {
mode: mode.CBC,
padding: pad.ZeroPadding,
iv
});
return aesResult.toString(enc.Utf8).trim();
};
<template>
<router-view v-slot="{ Component }">
<transition :name="animationName">
<keep-alive :include="cacheStore.cacheRouter">
<component :is="Component" />
</keep-alive>
</transition>
</router-view>
<van-number-keyboard safe-area-inset-bottom />
</template>
<script setup lang="ts">
import "views//app/hooks/useInitStyle";
import useSize from "views/app/hooks/useSize";
import useAnimation from "views/app/hooks/useAnimation";
import useCacheRouter from "@/store/useCacheRouter";
import useInitRequestToken from "views/app/hooks/useInitRequestToken";
import VConsole from "vconsole";
const ENV = import.meta.env;
if (ENV.MODE !== "production") new VConsole();
const { animationName } = useAnimation();
const cacheStore = useCacheRouter();
useSize();
useInitRequestToken();
</script>
<style lang="stylus">
#app
width 375px
height 100vh
overflow hidden
</style>
import { ref, watch } from "vue";
import router from "@/router";
import useAnimationStore from "@/store/useAnimationStore";
type AnimationName = "gIn" | "gOut" | "";
export default () => {
const animationStore = useAnimationStore();
const animationName = ref<AnimationName>("");
watch(
() => router.currentRoute.value.path,
() => {
if (!animationStore.animationData.isFirst) {
if (animationStore.animationData.trigger) {
animationName.value = animationStore.animationData.isBack ? "gOut" : "gIn";
}
} else {
animationStore.handleAnimationData({ isFirst: false });
}
}
);
return {
animationName
};
};
import { decode } from "@/utils/useCrypto";
import useRequestData from "@/store/useRequestData";
import useUrlParamStore from "@/store/useUrlParamStore";
import useCookieStore from "@/store/useCookieStore";
/**
* token优先级
* 1、第一级:使用url地址上的token(默认是已加密的,所以进行解密,如解析失败 ,则直接赋值)
* 2、第二级:使用cookie中的token
* 3、第三级:使用cookie中的userid
* 4、第四级:使用cookie中的app_token
*/
export default () => {
const cookieStore = useCookieStore();
const urlParamStore = useUrlParamStore();
const requestStore = useRequestData();
if (urlParamStore.urlData.token) {
try {
requestStore.handle("token", decodeURIComponent(decode(urlParamStore.urlData.token)));
} catch {
requestStore.handle("token", urlParamStore.urlData.token);
}
} else if (cookieStore.cookieData.token) {
requestStore.handle("token", cookieStore.cookieData.token?.toString() || "");
} else if (cookieStore.cookieData.userid) {
requestStore.handle("token", cookieStore.cookieData.userid);
} else if (cookieStore.cookieData.app_token) {
requestStore.handle("token", cookieStore.cookieData.app_token);
}
};
import "@/assets/css/init.styl";
import "@/assets/css/gAnimation.styl";
import "vant/es/toast/style";
import "vant/es/dialog/style";
import "vant/es/notify/style";
import "vant/es/image-preview/style";
import { useWindowSize } from "@vant/use";
import { debounce } from "lodash";
export default () => {
const ele: HTMLDivElement | null = document.querySelector("#app");
const setSize = debounce(() => {
const { width, height } = useWindowSize();
if (ele) {
ele.style.width = `${width.value}px`;
ele.style.height = `${height.value}px`;
}
}, 100);
setSize();
window.addEventListener("resize", setSize);
};
<template>
<div class="index" @click="closeWebApp">
<main @click.stop>
<Header :title-list="swiperData" :active="activeIndex" @setActive="setActiveIndex" />
<div class="container">
<div class="swiper removeScrollbar" ref="swiperRef">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="(item, index) in swiperData" :key="item.title">
<component
:is="item.component"
:index="index"
:currentIndex="activeIndex"
v-show-image="{
isBack: true,
src: item.backgroundImg
}"
/>
</div>
</div>
</div>
</div>
</main>
</div>
</template>
<script setup lang="ts">
import handleApp from "@/utils/HandleApp";
import Header from "views/index/components/header/Header.vue";
import usePageSwiper from "views/index/hooks/usePageSwiper";
const { swiperData, setActiveIndex, swiperRef, activeIndex } = usePageSwiper();
const closeWebApp = () => {
handleApp.closeWebview();
};
</script>
<style scoped lang="stylus">
.index
width 100%
height 100%
display flex
flex-direction column
justify-content flex-end
> main
width 100%
height 584px
display flex
flex-direction column
overflow hidden
>.container
flex 1
width 100%
> .swiper
width 100%
height 100%
</style>
<template>
<div class="card">
<ul>
<li v-for="i in prizeDraw.data" :key="i.key" :class="{ end: setSelect(i) }">
<div
v-show-image="{
isBack: true,
src: 'index/giftBg'
}"
:class="{
end: prizeDraw.number === -1
}"
>
<div class="imgBox">
<img :src="i.img" alt="" class="gift" />
</div>
<p v-show-image="{ isBack: true, src: 'index/giftName' }" class="one">
{{ i.name }}
</p>
</div>
<img
src=""
v-show-image="'index/card'"
alt=""
:class="{
end: prizeDraw.number === -1
}"
/>
</li>
</ul>
<img src="" v-if="prizeDraw.number === 0" class="charge" alt="" v-show-image="'index/charge'" @click="toRecharge" />
<img src="" class="charge" alt="" v-if="prizeDraw.number === 1" v-show-image="'index/btn1'" @click="emits('startPrizeDraw', false)" />
<div class="count">
<template v-if="prizeDraw.number === -1">
<div>今日抽卡機會已使用 <br />明日再來吧</div>
</template>
<template v-else-if="prizeDraw.number === 0">
<p>抽卡剩餘次數:0</p>
<img src="" v-show-image="'index/refresh'" :class="{ active }" alt="" @click="refresh" />
</template>
<template v-else>
<p>抽卡剩餘次數:1</p>
<img src="" v-show-image="'index/refresh'" :class="{ active }" alt="" @click="refresh" />
</template>
</div>
</div>
</template>
<script lang="ts" setup>
import PrizeDraw from "views/index/entities/PrizeDraw";
import Gift from "views/index/entities/Gift";
import handleApp from "@/utils/HandleApp";
interface IEmits {
(eventName: "startPrizeDraw", flag: boolean): void;
(eventName: "getPrizeDraw"): void;
}
interface IProps {
prizeDraw: PrizeDraw;
}
const emits = defineEmits<IEmits>();
const { prizeDraw } = defineProps<IProps>();
const active = ref(false);
const setSelect = (gift: Gift) => {
if (prizeDraw.number !== -1) return;
return gift.is === 1;
};
const toRecharge = () => {
handleApp.toRecharge(true);
};
const refresh = () => {
active.value = true;
setTimeout(() => {
active.value = false;
}, 1500);
emits("getPrizeDraw");
};
</script>
<style lang="stylus" scoped>
.card
width 350px
margin 0 auto
transform translateZ(0)
> ul
width 100%
height 226px
display flex
flex-wrap wrap
> li
width 84px
height 110px
margin-right 4px
margin-bottom 6px
position relative
&:nth-of-type(4n)
margin-right 0
> div
width 100%
height 100%
animation rotation 8s infinite linear
position absolute
top 0
left 0
z-index 11
background-size 100% 100%
background-position: center center
transform rotateY(180deg)
backface-visibility hidden
-webkit-backface-visibility hidden
> .imgBox
width 64px
height 64px
display flex
align-items center
justify-content center
margin 10px auto 4px auto
> img
max-width 64px
max-height 64px
> p
width 100%
height 22px
-webkit-background-size 100% 100%
background-size 100% 100%
text-align center
color #fff
font-size 10px
font-weight 600
line-height 22px
> img
width 100%
height 100%
position absolute
top 0
left 0
transform rotateY(0deg)
animation rotation 8s infinite linear
z-index 2
backface-visibility hidden
-webkit-backface-visibility hidden
animation-delay: -4s
> .end
animation none
&.end
> div
z-index 9
backface-visibility unset
transform rotateY(0)
> .charge
width 232px
display block
margin 16px auto 0 auto
> .count
width 100%
color #fff
font-size 12px
margin-top 5px
display flex
align-items center
justify-content center
> img
width 16px
height 16px
margin-left 10px
&.active
transition transform 1s linear
transform rotateZ(720deg)
> div
color #FFE89B
font-size 20px
font-weight 600
text-align center
line-height 1.5
margin-top 32px
@keyframes rotation
0%
transform rotateY(0)
100%
transform rotateY(360deg)
</style>
<template>
<div class="everydayGift" id="everydayGift">
<img src="" class="tips" alt="" v-show-image="'index/tips'" @click="setRuleShow(true)" v-show="!ruleShow" />
<main>
<Card :prizeDraw="prizeDraw" v-if="show" @startPrizeDraw="startPrizeDraw" @getPrizeDraw="getPrizeDraw" />
</main>
<!--底部规则-->
<Tips />
<!--规则弹窗-->
<Rule :show="ruleShow" @setShow="setRuleShow" />
<!--抽奖弹窗-->
<Lottery :show="!show" @endPrizeDraw="endPrizeDraw" :prize-draw="prizeDraw" />
</div>
</template>
<script lang="ts" setup>
import Tips from "views/index/components/tips/Tips.vue";
import Rule from "views/index/components/rule/Rule.vue";
import useRule from "views/index/hooks/useRule";
import Card from "views/index/components/card/Card.vue";
import Lottery from "views/index/components/lottery/Lottery.vue";
import usePrizeDraw from "views/index/hooks/usePrizeDraw";
interface IProps {
currentIndex: number;
}
const props = defineProps<IProps>();
const { setRuleShow, ruleShow } = useRule();
const { prizeDraw, show, startPrizeDraw, endPrizeDraw, getPrizeDraw } = usePrizeDraw({ setRuleShow });
watch(
() => props.currentIndex,
() => setRuleShow(false)
);
</script>
<style lang="stylus" scoped>
.everydayGift
width 100%
height 100%
background-repeat no-repeat
background-size 100%
overflow hidden
position relative
> .tips
position absolute
top 18px
right 18px
width 30px
height 30px
z-index 8989898
> main
width 352px
height 332px
margin 156px auto 19px auto
overflow hidden
display flex
flex-direction column
transform translateZ(0)
</style>
<template>
<div class="firstGift" id="firstGift">
<img src="" class="tips" alt="" v-show-image="'index/tips'" @click="setRuleShow(true)" v-show="!ruleShow" />
<main>
<ul class="removeScrollbar">
<li
v-for="(item, index) in giftList"
:key="item.src"
@click="setActiveIndex(index)"
:class="{
one: true,
active: activeIndex === index
}"
>
{{ item.title }}
</li>
</ul>
<div class="swiper removeScrollbar" ref="swiperRef">
<div class="swiper-wrapper">
<div
class="swiper-slide"
v-for="item in giftList"
:key="item.src"
v-show-image="{
isBack: true,
src: item.src
}"
>
<div
class="btn"
@click="buyFirstRechargeProduct(item)"
v-show-image="{
isBack: true,
src: 'index/btn'
}"
>
<template v-if="whetherPay === 1 || whetherPay === 2">
<span class="unit">{{ item.productLocaleSymbol }}</span>
<span class="unit">{{ item.productPrice }} {{$t("text1")}}</span>
<p class="original one" v-if="showOriginal()">
<span>{{$t("text2")}}</span>
<span>{{ item.productLocaleSymbol }} {{ computedPrice(item.productPrice * item.ratio) }}</span>
</p>
</template>
<template v-if="whetherPay === 0">
<span class="unit">{{$t("text3")}}</span>
</template>
</div>
</div>
</div>
</div>
</main>
<Tips />
<FirstRule :show="ruleShow" @setShow="setRuleShow" />
</div>
</template>
<script lang="ts" setup>
import Tips from "views/index/components/tips/Tips.vue";
import useGiftList from "views/index/components/firstGift/hooks/useGiftList";
import { round } from "lodash";
import FirstRule from "views/index/components/firstRule/FirstRule.vue";
import handleApp from "@/utils/HandleApp";
interface IProps {
currentIndex: number;
}
const { giftList, swiperRef, activeIndex, setActiveIndex, buyFirstRechargeProduct, whetherPay } = useGiftList();
const props = defineProps<IProps>();
const computedPrice = (n: number) => {
return round(n, 0);
};
const ruleShow = ref(false);
const setRuleShow = (show: boolean) => {
ruleShow.value = show;
};
watch(
() => props.currentIndex,
() => setRuleShow(false)
);
const showOriginal = () => {
return handleApp.agent === "android";
};
</script>
<style lang="stylus" scoped>
.firstGift
width 100%
height 100%
background-repeat no-repeat
background-size 100%
overflow hidden
position relative
> .tips
position absolute
top 18px
right 18px
width 30px
height 30px
z-index 99999
> main
width 343px
height 366px
background-color #fff
border-radius 12px
margin 128px auto 12px auto
overflow hidden
display flex
flex-direction column
transform translateZ(0)
> ul
width 316px
flex none
flex-wrap nowrap
margin 15px 13px 22px 13px
height 34px
overflow-x auto
font-size 0
white-space nowrap
box-sizing border-box
display flex
background-color #FFE5E1
border-radius 17px
> li
width 20%
height 100%
line-height 34px
font-size 13px
color #826B60
text-align center
transition padding .3s
font-weight 600
> i
margin 0 1px
&:last-of-type
margin-right 0
&.active
background-image linear-gradient(90deg, #FF9747 0%, #FF4646 100%)
border-radius 100vmax
color #fff
padding 0 9px
> .swiper
flex 1
width 100%
.swiper-slide
background-size 100%
height 100%
background-repeat no-repeat
background-position: top center
position relative
> .btn
position absolute
width 210px
height 48px
-webkit-background-size: 100%
background-size: 100%
background-position: center center
bottom 14px
left 50%
transform translateX(-50%)
color #fff
white-space nowrap
padding 0 8px
box-sizing border-box
display flex
align-items center
justify-content center
flex-wrap nowrap
> .unit
font-weight 600
font-size 16px
margin-right 4px
> .original
font-size 13px
font-weight 500
opacity .8
margin-top 5px
position relative
&::after
content " "
position absolute
top 50%
left 50%
transform translate(-50%, -50%)
width 100%
height 1px
background-color: #fff
</style>
import FirstGift from "entities/FirstGift";
import useSwiper from "views/index/hooks/useSwiper";
import handleApp from "@/utils/HandleApp";
import useCookieStore from "@/store/useCookieStore";
import { $queryFirstBuy } from "views/index/request";
import { showFailToast } from "vant";
import Tips from "entities/Tips";
import { useI18n } from "vue-i18n";
export default () => {
const { t } = useI18n();
const defaultGiftList = [
{
title: t("title1"),
src: "index/lv1",
ratio: 1.8
},
{
title: t("title2"),
src: "index/lv2",
ratio: 1.4
},
{
title: t("title3"),
src: "index/lv3",
ratio: 1.93333333
},
{
title: t("title4"),
src: "index/lv4",
ratio: 2.2
},
{
title: t("title5"),
src: "index/lv5",
ratio: 2.5
}
];
const giftList: FirstGift[] = reactive([]);
const whetherPay = ref<-1 | 0 | 1 | 2>(-1);
defaultGiftList.forEach(item => {
giftList.push(new FirstGift(item.title, item.src, item.ratio));
});
const cookieStore = useCookieStore();
const { swiperRef, setActiveIndex, activeIndex, init } = useSwiper();
// 调用原生api,跳转首充页面
const buyFirstRechargeProduct = (firstGift: FirstGift) => {
if (whetherPay.value === 2) return Tips.showToast({ message: t("tishi") });
if (whetherPay.value === 1) handleApp.buyFirstRechargeProduct(firstGift.productIdentifier);
};
const queryFirstBuy = async () => {
const { code, msg, data } = await $queryFirstBuy();
if (code !== 200) return showFailToast(msg);
whetherPay.value = data.status;
};
onBeforeMount(() => {
window.fetchFirstRechargeProduct = (data: string) => {
const list: FirstGift[] = JSON.parse(data);
console.log("获取原生价格列表", list);
list.forEach((item, index) => {
const current = giftList[index];
current.productIdentifier = item.productIdentifier;
current.productName = item.productName;
current.productLocaleSymbol = item.productLocaleSymbol;
current.productPrice = item.productPrice;
});
};
});
onMounted(queryFirstBuy);
onMounted(init);
onMounted(() => {
cookieStore.cookieData.platform === "app" && handleApp.fetchFirstRechargeProduct();
});
window.inApplicationIAPRechargeListener = () => {
console.log("用于测试, 原生调用web");
queryFirstBuy();
};
return {
giftList,
swiperRef,
activeIndex,
setActiveIndex,
buyFirstRechargeProduct,
whetherPay
};
};
<template>
<van-popup
v-if="mountFlag"
teleport="#firstGift"
:show="show"
:style="{ maxWidth: '100%', backgroundColor: 'transparent' }"
@click="emits('setShow', false)"
>
<main>
<img src="" alt="" v-show-image="'index/tips'" class="tips" />
<div
class="tipsBg"
v-show-image="{
isBack: true,
src: 'index/tipsBg'
}"
@click.stop
>
<h6>{{$t("guizhe")}}</h6>
<p>{{$t("guizhe1")}}</p>
<p>{{$t("guizhe2")}}</p>
<p>{{$t("guizhe3")}}</p>
<p>{{$t("guizhe4")}}</p>
<p>{{$t("guizhe5")}}</p>
</div>
</main>
</van-popup>
</template>
<script lang="ts" setup>
interface IProps {
show: boolean;
}
interface IEmits {
(eventName: "setShow", show: boolean): void;
}
const mountFlag = ref(false);
onMounted(() => {
mountFlag.value = true;
});
defineProps<IProps>();
const emits = defineEmits<IEmits>();
</script>
<style lang="stylus" scoped>
main
width 375px
min-height 540px
height auto
> .tips
position absolute
top 18px
right 18px
width 30px
height 30px
> .tipsBg
position absolute
top 70px
width 340px
height 297px
right 16px
background-size 100%
box-sizing border-box
padding 20px 15px 15px 15px
background-repeat no-repeat
> h6
width 100%
color #482A19
font-size 16px
font-weight 600
margin-bottom 8px
> p
color #675246
font-weight 500
font-size 12px
line-height 1.6
</style>
<template>
<header :style="{ backgroundColor: titleList[active].backgroundColor }">
<div v-for="(item, index) in titleList" :key="index" :class="{ active: index === active }" @click="emits('setActive', index)">
{{ item.title }}
</div>
</header>
</template>
<script lang="ts" setup>
import { SwiperData } from "views/index/hooks/usePageSwiper";
interface IEmits {
(eventName: "setActive", active: number): void;
}
interface IProps {
titleList: SwiperData[];
active: number;
}
defineProps<IProps>();
const emits = defineEmits<IEmits>();
</script>
<style lang="stylus" scoped>
header
width 100%
height 44px
border-radius 10px 10px 0 0
flex none
display flex
justify-content center
align-items center
transition background-color .3s
will-change background-color
> div
width 304px
height 100%
text-align center
line-height 44px
font-size 15px
font-weight 600
position relative
color #fff
&::after
position absolute
content " "
bottom 0
width 17px
border-radius 5px
height 4px
background-color: transparent
left 50%
transform translateX(-50%)
&.active
&::after
background-color: #fff
</style>
<template>
<van-popup :show="show" v-if="mounted" teleport="#everydayGift" :style="{ maxWidth: '100%', backgroundColor: 'transparent' }">
<main>
<div class="container" @click.stop>
<div
class="demo IBg"
v-show="sureFlag"
v-show-image="{
isBack: true,
src: 'index/gx'
}"
>
<template v-if="sureFlag">
<div
class="imgBox IBg"
:style="{
backgroundImage: `url(${gift.img})`
}"
></div>
<p v-show-image="{ isBack: true, src: 'index/selectBg' }" class="one IBg">
{{ gift.name }}
</p>
</template>
</div>
<ul ref="ulRef">
<li
v-for="(i, index) in prizeDraw.data"
:key="i.key"
@click="selectAnime(index)"
:class="{
hidden: index === selectIndex && sureFlag
}"
>
<div>
<img src="" v-show-image="'index/gx'" alt="" />
<div class="imgBox">
<img :src="gift.img" alt="" class="gift" />
</div>
<p v-show-image="{ isBack: true, src: 'index/selectBg' }" class="one">
{{ gift.name }}
</p>
</div>
<img src="" v-show-image="'index/card'" alt="" class="mask" />
</li>
</ul>
<p :class="{ active: showText }">請選擇一張卡</p>
<img src="" v-show-image="'index/sure'" alt="" class="sure" v-if="sureFlag" @click="emits('endPrizeDraw')" />
</div>
</main>
</van-popup>
</template>
<script lang="ts" setup>
import useAnime from "views/index/components/lottery/hooks/useAnime";
import PrizeDraw from "views/index/entities/PrizeDraw";
interface IEmits {
(eventName: "endPrizeDraw"): void;
}
interface IProps {
show: boolean;
prizeDraw: PrizeDraw;
}
const props = defineProps<IProps>();
const mounted = ref(false);
const { selectAnime, showText, ulRef, sureFlag, gift, selectIndex } = useAnime({
show: toRefs(props).show
});
const emits = defineEmits<IEmits>();
onMounted(() => {
mounted.value = true;
});
</script>
<style lang="stylus" scoped>
.IBg
background-repeat no-repeat
-webkit-background-size: cover
background-size: cover
background-position: contain
main
width 375px
height 540px
overflow hidden
> .container
width 352px
height 332px
margin 156px auto 19px auto
position relative
> .demo
position absolute
top -64px
left 50%
transform translateX(-50%) translateZ(0)
width 252px
height 330px
z-index 9999
> .imgBox
width 125px
height 125px
background-repeat no-repeat
-webkit-background-size: cover
background-size: contain
background-position: center center
margin 88px auto 36px auto
> p
width 181px
height 49px
margin 0 auto
text-align center
line-height 49px
color #fff
font-size 17px
> ul
width 100%
height 226px
display flex
flex-wrap wrap
position relative
animation: shake 0.82s linear
> li
width 84px
height 110px
margin-right 4px
margin-bottom 6px
position relative
transition opacity .3s
&.hidden
opacity 0
&:nth-of-type(4n)
margin-right 0
> div
width 100%
height 100%
position absolute
top 0
left 0
background-size 100% 100%
background-position: center center
backface-visibility hidden
transform rotateY(180deg)
z-index 11
> img
width 100%
height 100%
backface-visibility hidden
-ms-interpolation-mode: crisp-edges;
image-rendering: crisp-edges;
> .imgBox
width 44px
height 44px
display flex
align-items center
justify-content center
margin 28px auto 8px auto
position absolute
top 0
left 50%
transform translateX(-50%)
> img
max-width 100%
max-height 100%
> p
width 90%
height 22px
margin 0 auto
-webkit-background-size: 100% 100%
background-size: 100% 100%
text-align center
color #fff
font-size 8px
font-weight 600
line-height 22px
position absolute
bottom 10px
left 50%
transform translateX(-50%)
> img
width 100%
height 100%
position absolute
top 0
left 0
transform rotateY(0deg)
z-index 1
> p
text-align center
font-size 22px
color transparent
margin-top 20px
&.active
color #FFE89B
animation showText 2s 2
animation-fill-mode backwards
> .sure
width 232px
display block
margin 13px auto 0 auto
@keyframes shake {
10%, 90% {
transform: translate3d(-1px, -2px, 2px);
}
20%, 80% {
transform: translate3d(2px, 2px, -2px);
}
30%, 50%, 70% {
transform: translate3d(-4px, 0, 2px);
}
40%, 60% {
transform: translate3d(4px, 3px, -2px);
}
}
@keyframes showText
0%
opacity 0
50%
opacity 1
100%
opacity 0
</style>
import usePosition from "views/index/components/lottery/hooks/usePosition";
import anime from "animejs";
import usePxToVw from "@/utils/usePxToVw";
import { Ref } from "vue";
import { $handlePrizeDraw } from "views/index/request";
import Tips from "entities/Tips";
import Gift from "views/index/entities/Gift";
import { ToastWrapperInstance } from "vant";
interface Params {
show: Ref<boolean>;
}
export default ({ show }: Params) => {
const showText = ref(false);
const ulRef = ref<HTMLUListElement>();
const { startAnimeP } = usePosition();
const gift: Gift = reactive(new Gift());
let selectFlag = true;
const sureFlag = ref(false);
const selectIndex = ref(-1);
// 调用抽奖接口
const handlePrizeDraw = async (index: number) => {
const { code, msg, data } = await $handlePrizeDraw(index);
if (code !== 200) {
Tips.showToast(msg, "fail");
return false;
}
return data;
};
const startAnime = () => {
const ulEle = ulRef.value;
if (ulEle) {
const lis = ulEle.querySelectorAll("li");
for (let i = 0; i < lis.length; i++) {
const t = anime.timeline({});
t.add({
targets: lis[i],
translateX: usePxToVw(startAnimeP[i].x),
translateY: usePxToVw(startAnimeP[i].y),
delay: i * 50
});
t.add({
targets: lis[i],
translateX: 0,
translateY: 0,
delay: i * 50,
update(anim) {
if (i === lis.length - 1 && anim.progress === 100) {
showText.value = true;
selectFlag = false;
}
}
});
}
} else {
nextTick(startAnime);
}
};
// 选中弹开
const selectAnime = async (index: number) => {
if (selectFlag) return;
const loading: ToastWrapperInstance = Tips.showLoading({
duration: 0
});
const result = await handlePrizeDraw(index);
loading.close();
if (result) {
sureFlag.value = true;
selectFlag = true;
const g = Gift.transform(Gift, result);
const errors = await g.validator();
if (errors.length > 0) return Tips.showToast(errors[0], "fail");
Object.assign(gift, g);
selectFlag = true;
sureFlag.value = true;
selectIndex.value = index;
/*const ulEle = ulRef.value;
if (ulEle) {
const ele = ulEle.querySelectorAll("li")[index];
const div = ele.querySelector("div");
const imgEle = ele.querySelector(".mask");
const duration = 1;
if (ele && div && imgEle) {
anime({
targets: ele,
scale: 3,
translateX: selectAnimeP[index].x,
translateY: selectAnimeP[index].y,
zIndex: [9, 9],
duration
});
anime({
targets: div,
rotateY: [0, 360],
duration
});
anime({
targets: imgEle,
rotateY: [0, 360],
duration,
update(anim) {
if (anim.progress === 100) {
sureFlag.value = true;
selectIndex.value = index;
}
}
});
}
}*/
}
};
watch(show, value => {
if (value) {
setTimeout(startAnime, 1000);
}
});
return {
showText,
ulRef,
startAnime,
selectAnime,
sureFlag,
gift,
selectIndex
};
};
export default () => {
const startAnimeP = [
{
x: 130,
y: 55
},
{
x: 46,
y: 55
},
{
x: -38,
y: 55
},
{
x: -122,
y: 55
},
{
x: 146,
y: -60
},
{
x: 62,
y: -60
},
{
x: -22,
y: -60
},
{
x: -106,
y: -60
}
];
const selectAnimeP = [
{
x: 45,
y: 16
},
{
x: 15,
y: 16
},
{
x: -14,
y: 16
},
{
x: -43,
y: 16
},
{
x: 45,
y: -24
},
{
x: 15,
y: -24
},
{
x: -14,
y: -24
},
{
x: -43,
y: -24
}
];
return {
startAnimeP,
selectAnimeP
};
};
<template>
<van-popup
v-if="mountFlag"
teleport="#everydayGift"
:show="show"
:style="{ maxWidth: '100%', backgroundColor: 'transparent' }"
@click="emits('setShow', false)"
>
<main>
<img src="" alt="" v-show-image="'index/tips'" class="tips" />
<div
class="tipsBg"
v-show-image="{
isBack: true,
src: 'index/tipsBg'
}"
@click.stop
>
<h6>規則說明</h6>
<p>1.每位用戶每日首次充值,即可獲得一次抽卡機會; (每日僅可獲得一次抽卡機會,當日未使用,機會作廢)</p>
<p>
2.參與抽卡機會的用戶100%獲得獎勵,獎品隨機,所獲得的獎品發放至用戶賬戶或背包;<br />
(獲得獎品以實際抽到為主,平臺有權基於運營情況,對獎勵進行調整)
</p>
<p>3.平臺禁止一切作弊、外掛等違反平臺規則的行為,一經發現,取消獎勵,並給予相應懲罰;</p>
<p>4.活動時間以標準時區UTC+8為準,其他時區用戶請註意時差。活動解釋權歸平臺所有;</p>
<p>5.本次活動與Apple Inc.或Google LLC無關。</p>
</div>
</main>
</van-popup>
</template>
<script lang="ts" setup>
interface IProps {
show: boolean;
}
interface IEmits {
(eventName: "setShow", show: boolean): void;
}
const mountFlag = ref(false);
onMounted(() => {
mountFlag.value = true;
});
defineProps<IProps>();
const emits = defineEmits<IEmits>();
</script>
<style lang="stylus" scoped>
main
width 375px
height 540px
> .tips
position absolute
top 18px
right 18px
width 30px
height 30px
> .tipsBg
position absolute
top 70px
width 312px
height 287px
right 16px
background-size 100%
box-sizing border-box
padding 20px 15px 15px 15px
> h6
width 100%
color #482A19
font-size 16px
font-weight 600
margin-bottom 8px
> p
color #675246
font-weight 500
font-size 12px
line-height 1.6
</style>
<template>
<div>{{ str }}</div>
</template>
<script lang="ts" setup>
import handleApp from "@/utils/HandleApp";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const str = handleApp.agent === "ios" ? t("appletext") : t("gooletext");
</script>
<style lang="stylus" scoped>
div
text-align center
color #fff
font-size 11px
</style>
import BaseEntities from "entities/BaseEntities";
import { IsInt, IsString } from "class-validator";
import useErrorMessage from "@/utils/useErrorMessage";
export default class Gift extends BaseEntities {
@IsString({ message: useErrorMessage("img", "string") })
img = "";
@IsString({ message: useErrorMessage("name", "string") })
name = "";
@IsString({ message: useErrorMessage("key", "string") })
key = "";
// -1表示未抽中 1 表示已抽中
@IsInt({ message: useErrorMessage("is", "int") })
is = -1;
}
import { IsArray, IsInt, ValidateNested } from "class-validator";
import useErrorMessage from "@/utils/useErrorMessage";
import Gift from "views/index/entities/Gift";
import { Type } from "class-transformer";
import BaseEntities from "entities/BaseEntities";
export default class PrizeDraw extends BaseEntities {
@IsInt({ message: useErrorMessage("number", "int") })
number = -1;
@IsArray({ message: useErrorMessage("data", "array") })
@ValidateNested({ each: true })
@Type(() => Gift)
data: Gift[] = [];
}
// import EverydayGift from "views/index/components/everydayGift/EverydayGift.vue";
import FirstGift from "views/index/components/firstGift/FirstGift.vue";
import useSwiper from "views/index/hooks/useSwiper";
import { useI18n } from "vue-i18n";
export interface SwiperData {
title: string;
component: Component;
backgroundColor: string;
backgroundImg: string;
}
export default () => {
const { t } = useI18n();
const swiperData: SwiperData[] = [
{
title: t("tag"),
component: FirstGift,
backgroundColor: "#F8505D",
backgroundImg: "index/one"
}
];
const { activeIndex, setActiveIndex, swiperRef, init } = useSwiper();
onMounted(() => {
init({
allowTouchMove: false,
initialSlide: 0
});
});
return {
swiperData,
activeIndex,
setActiveIndex,
swiperRef
};
};
import PrizeDraw from "views/index/entities/PrizeDraw";
import { $getPrizeDraw } from "views/index/request";
import Tips from "entities/Tips";
import { ToastWrapperInstance } from "vant";
interface Props {
setRuleShow: (flag: boolean) => void;
}
export default ({ setRuleShow }: Props) => {
const prizeDraw: PrizeDraw = reactive(new PrizeDraw());
const getPrizeDraw = async (): Promise<ToastWrapperInstance | undefined> => {
const { data, code, msg } = await $getPrizeDraw();
if (code !== 200) return Tips.showToast(msg, "fail");
const p: PrizeDraw = PrizeDraw.transform(PrizeDraw, data);
const errors: string[] = await p.validator();
if (errors.length > 0) return Tips.showToast(errors[0], "fail");
Object.assign(prizeDraw, p);
};
onMounted(getPrizeDraw);
const show = ref(true);
// 开始抽奖
const startPrizeDraw = (flag: boolean) => {
show.value = flag;
};
// 抽奖完毕点击收下
const endPrizeDraw = async () => {
await getPrizeDraw();
show.value = true;
setRuleShow(false);
};
return {
prizeDraw,
startPrizeDraw,
show,
endPrizeDraw,
getPrizeDraw
};
};
export default () => {
const ruleShow = ref(false);
const setRuleShow = (show: boolean) => {
ruleShow.value = show;
};
return {
ruleShow,
setRuleShow
};
};
import { Swiper } from "swiper";
import "swiper/swiper-bundle.css";
import type { SwiperOptions } from "swiper/types/swiper-options";
export default () => {
let swiper: Swiper;
const swiperRef = ref<HTMLDivElement>();
const activeIndex = ref(0);
const setActiveIndex = (index: number) => {
if (swiper) {
swiper.slideTo(index);
activeIndex.value = index;
} else {
init();
nextTick(() => {
setActiveIndex(index);
});
}
};
const init = (option: SwiperOptions = {}) => {
const ele = swiperRef.value;
if (ele) {
swiper = new Swiper(ele, {
on: {
slideChange(e: Swiper) {
activeIndex.value = e.activeIndex;
}
},
...option
});
}
};
return {
init,
setActiveIndex,
activeIndex,
swiperRef
};
};
export default {
title: "首页"
};
enum Apis {
handlePrizeDraw = "/api/User/userEverydayPrizeDraw",
getPrizeDraw = "/api/Pay/queryEverydayPrizeDrawNumber",
queryFirstBuy = "/api/Pay/queryFirstBuy"
}
export default Apis;
import { post } from "@/request";
import Apis from "views/index/request/apis";
import PrizeDraw from "views/index/entities/PrizeDraw";
import Gift from "views/index/entities/Gift";
export const $handlePrizeDraw = (subscript: number) => post<Gift>(Apis.handlePrizeDraw, { subscript });
export const $getPrizeDraw = () => post<PrizeDraw>(Apis.getPrizeDraw);
// 是否可以购买 1可以购买,0不可以购买 2显示可以购买按钮但是用户点击提示不符合購買要求
export const $queryFirstBuy = () => post<{ status: 1 | 0 | 2 }>(Apis.queryFirstBuy);
/// <reference types="vite/client" />
interface OpenInstallOption {
appKey: string; // appKey必选参数,OpenInstall为每个应用分配的ID
channelCode?: string; // 直接指定渠道编号,默认通过当前页url中的channelCode参数自动检测渠道编号
mask?: () => string;
onready?: () => string;
}
declare interface Window {
// 获取首充礼包数据(app回调)
fetchFirstRechargeProduct: (data: string) => void;
inApplicationIAPRechargeListener: (callback: () => void) => void;
webkit: {
messageHandlers: {
openShareTip: {
postMessage: (param) => void; // ios分享
};
closeWeb: {
postMessage: (data) => void; // ios关闭webview
};
toPerson: {
postMessage: (data) => void;
};
actionAsPhp: {
// 进入个人中心 / 支付
postMessage: (param) => void;
};
toLiveroom: {
// 进入直播间
postMessage: (param) => void;
};
getStatusBarHeight: {
postMessage: () => number;
};
selecetTabBarItem: {
postMessage: (param: { index: number | string }) => void;
};
fetchFirstRechargeProduct: {
postMessage: (p: boolean) => void;
};
buyFirstRechargeProduct: {
postMessage: (p: { productIdentifier: string }) => void;
};
};
};
liveapp: {
openShareTip: (param: string) => void;
closeWeb: () => void;
toPerson: (text) => void;
actionAsPhp: (str: string) => void;
toLiveroom: (param: string) => void;
getStatusBarHeight: () => number;
selecetTabBarItem: (param: string) => void;
fetchFirstRechargeProduct: () => void;
buyFirstRechargeProduct: (productIdentifier: string) => void;
};
OpenInstall;
}
/**
* 这是一个倒计时的worker
* 入参是一个对象(使用的时候用注意JSON.stringify) 如: {
* type: string,
* data: jsonString
* }
* 当前函数 只接收type等于countDown的消息
* data转成对象之后的数据结构如下: {
* targetTimeEventName: string,
* targetTime: string | number
* }
*/
interface Data {
targetTimeEventName: string;
targetTime: string | number;
}
import dayjs, { Dayjs } from "dayjs";
self.onmessage = ev => {
try {
const { type, data } = ev.data;
// 倒计时函数
if (type === "countDown") {
const { targetTimeEventName, targetTime } = JSON.parse(data) as Data;
if (!targetTimeEventName || !targetTime) {
self.postMessage({
type: "error",
message: "定时器参数不正确"
});
return;
}
countDown(dayjs(targetTime), targetTimeEventName);
}
} catch {
self.postMessage({
type: "error",
message: "定时器参数不正确"
});
}
};
// 倒数计时
const countDown = (targetTime: Dayjs, targetTimeEventName: string) => {
let timer = 0;
const handleCount = () => {
if (targetTime <= dayjs()) {
//时间到了
cancelAnimationFrame(timer);
timer = 0;
self.postMessage({
type: "countDown",
data: JSON.stringify({
targetTimeEventName
})
});
} else {
timer = requestAnimationFrame(handleCount);
}
};
handleCount();
};
export {};
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"noEmit": true,
"emitDecoratorMetadata": true,
"declaration": true,
"experimentalDecorators": true,
"paths": {
"@/*": ["./src/*"],
"components/*": ["./src/components/*"],
"views/*": ["./src/views/*"],
"entities/*": ["./src/entities/*"],
"assets": ["./src/assets/*"],
"public": ["./public/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import Components from "unplugin-vue-components/vite";
import { VantResolver } from "unplugin-vue-components/resolvers";
import { resolve, extname } from "path";
import postcssPxToViewport from "postcss-px-to-viewport";
import eslintPlugin from "vite-plugin-eslint";
import AutoImport from "unplugin-auto-import/vite";
// @ts-ignore
import jsonObj from "./package.json";
import { readdirSync, rmSync } from "fs";
const mediaReg = /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/i;
const imgReg = /\.(png|jpe?g|gif|svg|webp)(\?.*)?$/;
const fontReg = /\.(woff2?|eot|ttf|otf)(\?.*)?$/i;
const dir = resolve(__dirname);
// 读取要删除的函数
function readDeleteDir(path: string) {
readdirSync(path).forEach(item => {
if (item.includes(jsonObj.name)) {
const url = `${path}/${item}`;
rmSync(url, { recursive: true });
}
});
}
export default ({ mode }) => {
if (mode !== "development") readDeleteDir(dir);
const plugins = [
vue({
script: {
propsDestructure: true
}
}),
Components({
resolvers: [VantResolver()]
}),
AutoImport({
imports: ["vue", "vue-router"],
dts: "./src/autoImport.d.ts"
})
];
if (mode === "development") {
plugins.push(
eslintPlugin({
exclude: [resolve("./src/!*.d.ts"), resolve("foreign-country-utils/!*")]
})
);
}
const compress = {
drop_console: false,
drop_debugger: true
};
if (mode === "production") {
compress.drop_console = true;
}
return defineConfig({
plugins,
css: {
postcss: {
plugins: [
postcssPxToViewport({
unitToConvert: "px", // 要转化的单位
viewportWidth: 375, // UI设计稿的宽度
unitPrecision: 6, // 转换后的精度,即小数点位数
propList: ["*"], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
viewportUnit: "vw", // 指定需要转换成的视窗单位,默认vw
fontViewportUnit: "vw", // 指定字体需要转换成的视窗单位,默认vw
selectorBlackList: ["ignore-"], // 指定不转换为视窗单位的类名,
minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false
replace: true, // 是否转换后直接更换属性值
exclude: [],
landscape: false // 是否处理横屏情况
})
]
},
preprocessorOptions: {
stylus: {
imports: [resolve("./src/assets/css/public.styl")]
}
}
},
resolve: {
alias: {
"@": resolve(__dirname, "./src/"),
components: resolve(__dirname, "./src/components/"),
views: resolve(__dirname, "./src/views/"),
entities: resolve(__dirname, "./src/entities"),
public: resolve(__dirname, "public")
}
},
build: {
minify: "terser",
terserOptions: {
compress
},
target: "es2015",
rollupOptions: {
output: {
assetFileNames: assetInfo => {
const { name } = assetInfo;
let extType = extname(name).substring(1);
if (mediaReg.test(name)) {
extType = "media";
} else if (imgReg.test(name)) {
extType = "img";
} else if (fontReg.test(name)) {
extType = "fonts";
}
return `${extType}/[name]-[hash][extname]`;
},
chunkFileNames: "js/[name]-[hash].js",
entryFileNames: "js/[name]-[hash].js",
dir: `${mode}-${jsonObj.name}`
}
}
},
base: "./"
});
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment