VUE工程目录中,一个模块一般包含三个文件:main.ts、App.vue、index.html。
// main.ts 源码为:
import Vue from 'vue';
import App from './App.vue';
import '@/assets/styles/common.less';
import { getEnvStatus } from '@/utils/tools';
import waterfall from 'vue-waterfall2';
import 'swiper/css/swiper.css';
import i18n from '@/i18n';
if (getEnvStatus().isRelease) {
// 阻止vue启动时生成生产提示
Vue.config.devtools = false;
} else {
Vue.config.productionTip = true;
Vue.config.devtools = true;
}
Vue.use(waterfall);
new Vue({
i18n,
render: h => h(App)
}).$mount('#app');
// index.html 源码为:
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8" />
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no,viewport-fit=cover" />
<meta name="format-detection" content="telephone=no" />
<meta name="apple-touch-fullscreen" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="full-screen" content="yes" />
<meta name="x5-fullscreen" content="true" />
<meta name="360-fullscreen" content="true" />
<title>
</title>
<%= htmlWebpackPlugin.options.htmlAppend %>
<script type="text/javascript" crossorigin="anonymous"
src="<%= VUE_APP_BASE_URL %>js/toos.js"></script>
</head>
<body>
<noscript>
<strong></strong>
</noscript>
<div id="app"></div>
<!-- 黑白模型 -->
<script src="<%= VUE_APP_BASE_URL %>js/thsc-theme.min.js"></script>
<link href="<%= VUE_APP_BASE_URL %>css/thsc-theme-ths.min.css">
<!-- built files will be auto injected -->
<script src="<%= VUE_APP_BASE_URL %>js/thsc-base-jssdk.js"></script>
</body>
</html>
// App.vue 源码为:
<template>
<div>
<div v-if="pageType == 0" class="ai-cartoon-page">
<Launch v-if="showType === AiToolScene.None"
:title="toolName"
:slogan="toolSlogan"
:imgSrc="displayImage"
@button-clicked="handleSelectPhoto"/>
<!-- 3D卡通 笑脸特效-->
<CartoonPage
v-if="showType === AiToolScene.Cartoon"
:toolType="toolType"
:title="toolName"
:faceImage="selecteFaceImage"
:imageDataUrlScheme="processImage.imageDataUrlScheme"
:imageFilePath="processImage.imagePath"
@next-button-clicked="showResultPage"></CartoonPage>
</div>
<!-- 结果页 -->
<Product
v-if="pageType == 1"
:resultImage="resultImagePath"
> </Product>
</div>
</template>
<script lang="ts" setup>
import Launch from './components/Launch/index.vue';
import CartoonPage from './components/Cartoon/index.vue';
import Product from './components/product/index.vue';
import {ref, onMounted, computed, getCurrentInstance } from 'vue';
import { AiToolScene } from '@/types/enum';
import { AIToolModelDetail, AIToolResult, PhotoImage } from './types/model';
import {
callNativeFunction,
getClientUserInfo,
openPhotoImage,
setupWebViewJavascriptBridge,
startSynthesizing
} from '@/utils/clientBridge';
import { apiPostRequest } from '@/utils/apiRequest';
import config from '@/config';
import { getUserInfo } from "@/constants";
// 获取当前实例
const { proxy } = getCurrentInstance()!;
const toolSlogan = ref('');
const pageType = ref(0);
const toolName = ref('');
let toolType = ref(AiToolScene.None);
let showType = ref(AiToolScene.None);
const processImage = ref();
const selecteFaceImage = ref('');
const resultImagePath = ref('');
let toolDetail: AIToolModelDetail;
let templateId: string;
onMounted( () => {
const searchParams = new URLSearchParams(window.location.search);
const toolId = searchParams.get('toolId');
if (/Android/i.test(navigator.userAgent)) {
requestDetail(toolId);
} else {
// iOS需要独立注册
setupWebViewJavascriptBridge( () => {
requestDetail(toolId);
});
}
});
async function requestDetail(toolId: string) {
apiPostRequest<AIToolModelDetail>(config.api.getToolDetail, {
tool_id: toolId,
}).then(res => {
toolSlogan.value = res.subtitle;
toolName.value = res.toolName;
templateId = res.extParams ? res.extParams.templateId : '';
toolDetail = res;
});
const userInfo = await getUserInfo();
proxy.$i18n.locale = userInfo.language;
if (/Android/i.test(navigator.userAgent)) {
const h = userInfo.statusBarHeight ? userInfo.statusBarHeight : 26;
document.querySelector('.ai-cartoon-page').style.paddingTop = h + "px";
}
}
const displayImage = computed(() => {
const searchParams = new URLSearchParams(window.location.search);
const paramType = searchParams.get('type');
if (paramType === "cartoon") {
toolType.value = AiToolScene.Cartoon;
return "cartoon.png";
}
if (paramType === "smile") {
toolType.value = AiToolScene.Smile;
return 'smile.webp';
}
if (paramType === "enhance") {
toolType.value = AiToolScene.ENHANCE;
return 'enhance.webp';
}
return '';
});
function handleSelectPhoto() {
startPhotoAsync();
}
async function startPhotoAsync() {
try {
const photoParams = {
maxSelectCount: 1,
faceCount: toolDetail.inputConfig.imageConfig.faceMaxCount,
demoPaths: [toolDetail.cartoonDemoAwsImg],
userRecentPhoto: true,
doCrop: false,
darkMode: true
}
const result = await openPhotoImage(photoParams);
if (result) {
const selectImage = result.faceImages[0];
if (toolType.value === AiToolScene.ENHANCE) {
if (templateId.length > 0) {
let extParams = toolDetail.extParams;
const params = {
animateChannel: "common",
workName: toolDetail.toolName,
templateInfo: {
play_types: extParams.playTypes,
id: extParams.templateId
},
imageInfo: {
localPath: selectImage.imagePath
}
};
startSynthesizing(params);
}
return;
}
processImage.value = selectImage;
selecteFaceImage.value = processImage.value.faceImage;
showType.value = AiToolScene.Cartoon;
}
} catch (error) {
window.console.error(error);
}
}
const showResultPage = (path) => {
resultImagePath.value = path;
pageType.value = 1;
};
</script>
<style lang="less" scoped>
@import '~@/assets/styles/common.less';
.ai-cartoon-page {
background-color: #03141A;
padding-top: env(safe-area-inset-top);
height: 100vh;
overflow: hidden;
font-family: "Poppins";
background-image: url('~@/assets/images/aitool/bg.png');
background-size: contain;
background-position: top;
background-repeat: no-repeat;
}
*{
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
user-select: none; /* Non-prefixed version, currently supported by Chrome, Opera and Edge */
-webkit-touch-callout: none; /* iOS Safari */
}
</style>
他们三个是什么关系呢?
在一个典型的 Vue.js 项目中,main.ts、index.html 和 App.vue 这三个文件共同构成了应用的核心结构。
- index.html:
这是应用的主 HTML 模板文件。它定义了应用的基本结构,包括 <head> 和 <body> 标签,设置了元信息(如字符集、视口配置等),并在 <body> 中包含一个 id 为 app 的 <div> 元素。
Vue.js 应用会在这个 <div> 元素中挂载和渲染整个应用的内容。
- main.ts:
这是 Vue 应用的入口文件。它是第一个被执行的文件,负责初始化 Vue 实例并将其挂载到 index.html 中的 #app 元素上。
在 main.ts 中,通常会导入全局的样式、第三方库(如 vue-waterfall2 和 i18n),并配置 Vue 实例(如关闭生产环境提示)。
最后,它通过 render 函数渲染 App.vue 组件,并将其挂载到 #app 元素。
- App.vue:
这是应用的根组件,所有的子组件都将由这个组件管理。App.vue 通常定义了应用的基本布局和逻辑。
在 main.ts 中被导入,并通过 Vue 实例的 render 函数进行渲染,最终显示在 index.html 的 #app 元素中。
总结来说,index.html 提供了基础 HTML 结构,main.ts 负责初始化 Vue 应用并将 App.vue 渲染到 index.html 中,App.vue 则定义了应用的核心 UI 和业务逻辑。
另外,App.vue下面所包含的子模块的代码结构
App.vue下面所包含的子模块中有一个Launch/index.vue,代码如下:
<template>
<div class="flex-start-column">
<TopBar :title="title" :showShare='false' :showMore='false' backgroundColor="transparent"/>
<img :src="imgPath" class="cartoon-image"/>
<p class="slogan">{{ slogan }}</p>
<button
class="action-button action-button-gradient"
@click="clickSelectPhoto"
:disabled="slogan.length == 0"
:style="{opacity: slogan.length == 0 ? '0.5' : '1'}"
>
{{ $t('message.pickPhoto') }}
</button>
</div>
</template>
<script setup lang="ts">
import { computed, defineEmits, onMounted, ref } from 'vue';
import TopBar from '@/components/TopBar/index.vue';
const props = defineProps<{
slogan: string
imgSrc: string
title: string
}>()
const emit = defineEmits<{
(event: 'button-clicked'): void;
}>();
const imgPath = computed(() => {
return require("@/assets/images/aitool/" + props.imgSrc);
});
const clickSelectPhoto = () => {
emit('button-clicked');
}
</script>
<style scoped lang="less">
@import '~@/assets/styles/common.less';
.flex-start-column {
display: flex;
justify-content: start;
flex-direction: column;
align-items: center;
}
.cartoon-image {
width: calc(100% - 96px);
height: auto;
aspect-ratio: 1/1;
margin: 64px 48px 0px;
border-radius: 16px;
object-fit: cover;
}
.slogan {
margin-top: 32px;
font-size: 18px;
font-weight: 500;
height: 27px;
color: #FFFFFF;
}
.action-button {
height: 54px;
margin: 35px 67px;
width: calc(100% - 134px);
border-radius: 27px;
color: white;
border: none;
font-size: 18px;
font-weight: 600;
text-align: center;
flex-grow: 1;
}
// 普通用户渐变背景
.action-button-gradient {
background: linear-gradient(to right, #00CCBB, #00E586);
}
// VIP背景
.action-button-gradient-pro {
background: linear-gradient(to right, #18EE92, #2FC4D9, #3D7CFF);
}
</style>
大致分为三个大部分:<template>、<script setup lang="ts">、<style scoped lang="less">,其中<script>内的方法可以互调,最后<script>、<style>都会被<template>统一调用。