初始化

This commit is contained in:
2025-01-27 23:24:42 +08:00
parent c8a96306c4
commit 6ae8339299
421 changed files with 35687 additions and 0 deletions

113
components/AppFooter.vue Normal file
View File

@@ -0,0 +1,113 @@
<template>
<footer class="overflow-hidden pt-4">
<!-- 间隔 -->
<div class="sm:h-[3px] h-[15px]" v-if="getPath().startsWith('/passport/login')"></div>
<div class="sm:h-[30px] h-[15px]" v-else></div>
<div class=" bg-white my-3 w-3/4 m-auto" v-if="config.copyrightForDemoData">
<el-alert :title="config.copyrightForDemoData" center show-icon type="warning" />
</div>
<div class="w-full flex flex-col sm:bg-black justify-between">
<!-- PC版 -->
<div class="sub-menu w-full xl:w-screen-xl xl:px-0 px-4 m-auto flex justify-between py-10 text-center hidden-sm-and-down">
<!-- 底部菜单 -->
<div class="left flex flex-col sm:flex-row sm:flex-wrap justify-between gap-3xl sm:w-9/12 flex-wrap w-full">
<template v-for="item in subMenu">
<div class="sub-menu-item text-left">
<div class="pb-4">
<text class="text-gray-400 hover:text-gray-300 font-bold text-[16px]">{{ item.title }}</text>
</div>
<template v-if="item.children">
<div class="sub-menu-children flex flex-col gap-xs">
<template v-for="sub in item.children">
<a @click="openSpmUrl(`${sub.path}`,sub,sub.navigationId)" class="text-gray-400 hover:text-gray-300 cursor-pointer">{{ sub.title }}</a>
</template>
</div>
</template>
</div>
</template>
</div>
<!-- 关注我们 -->
<div class="right w-3/12 pr-3 text-right flex justify-end" v-if="config.wxQrcode">
<div class="qrcode flex flex-col items-center">
<el-image :src="config.wxQrcode" class="w-[100px]" />
<text class="text-gray-400 py-2">{{ config.wxQrcodeText }}</text>
</div>
</div>
</div>
<div class="flex pt-1 w-auto sm:hidden px-4 sm:px-0">
<el-divider><span class="text-gray-400">Footer</span></el-divider>
</div>
<div class="mx-3 sm:hidden p-5 bg-white rounded-xl">
<el-collapse v-model="activeNames" accordion>
<template v-for="(item,index) in subMenu">
<el-collapse-item :title="item.title" :name="index">
<template v-if="item.children">
<template v-for="sub in item.children">
<a :href="sub.path" class="block cursor-pointer text-gray-600 hover:text-gray-900">{{ sub.title }}</a>
</template>
</template>
</el-collapse-item>
</template>
</el-collapse>
</div>
<!-- <div class="sm:hidden p-4 mt-3">-->
<!-- <el-button type="primary" class="w-full" size="large">联系我们</el-button>-->
<!-- </div>-->
<!-- 版权信息 -->
<div class="w-full xl:w-screen-xl xl:px-0 px-4 w-full m-auto flex sm:flex-row flex-col-reverse sm:justify-between justify-center items-center sm:py-10 pt-6 pb-6 text-center">
<div class="text-gray-400 sm:gap-xl leading-7 flex flex-col sm:flex-row">
<span>Copyright © {{ new Date().getFullYear() }} {{ config?.copyright }}</span>
<a class="text-gray-400 hover:text-gray-400" href="https://beian.miit.gov.cn/" target="_blank"> 备案号{{ config?.icpNo }}</a>
</div>
<div class="tools flex gap-xl items-center opacity-80 hover:opacity-90 text-gray-400 text-sm">
Powered by <a rel="nofollow" href="https://website.websoft.top" target="_blank" class="text-gray-400 hover:text-gray-50">·企业官网</a>
<!-- <el-tooltip :content="`管理后台`" v-if="config.showAdminIcon">-->
<!-- <a :href="`https://${website.tenantId}.websoft.top`" target="_blank"><img src="@/assets/svg/websoft-mark-white.svg" alt="github" width="28" class="text-gray-400" /></a>-->
<!-- </el-tooltip>-->
</div>
</div>
</div>
</footer>
<footer class="absolute bottom-0 w-full" v-if="getPath().startsWith('/login')">
<div class="w-full xl:w-screen-xl xl:p-0 px-4 w-full m-auto flex sm:flex-row flex-col-reverse sm:justify-center justify-center items-center sm:py-10 pt-6 pb-6 text-center">
<div class="text-gray-400 sm:gap-xl leading-7 flex flex-col sm:flex-row">
<span>Copyright © {{ new Date().getFullYear() }} {{ config?.copyright }}</span>
<a class="text-gray-400 hover:text-gray-400" href="https://beian.miit.gov.cn/" target="_blank"> 备案号{{ config?.icpNo }}</a>
</div>
</div>
</footer>
<el-backtop></el-backtop>
</template>
<script setup lang="ts">
// 请求数据
import {getPath, openSpmUrl, useConfigInfo} from "#imports";
import {useSubMenu, useWebsite} from "~/composables/configState";
import { StarFilled } from '@element-plus/icons-vue'
const config = useConfigInfo();
const website = useWebsite();
const subMenu = useSubMenu();
console.log('---------config---------',config.value)
console.log('---------website---------',website.value)
console.log('---------subMenu---------',subMenu.value)
const activeNames = ref(['1'])
</script>
<style lang="scss">
.el-divider__text{
display: block;
margin-top: -1px;
--el-bg-color: #ffffff;
}
</style>

233
components/AppHeader.vue Normal file
View File

@@ -0,0 +1,233 @@
<template>
<el-affix :offset="0" @change="onAffix">
<header
class="header z-100 top-0 w-full bg-white/75 opacity-90 backdrop-blur border-b border-gray-200 dark:border-gray-800 -mb-px sticky top-0 z-50 lg:mb-0 lg:border-0"
:class="affix ? 'absolute blur-xs' : 'sticky bg-white/75'">
<div class="xl:w-screen-xl xl:p-0 px-4 flex items-center between w-full m-auto">
<div class="header___left flex items-center">
<div class="logo mt-1 sm:w-[150px] h-7 w-auto py-2 flex items-center">
<nuxt-link v-if="config?.siteLogo" to="/">
<div class="flex flex-col text-center xl:p-0">
<el-image
:src="config.siteLogo"
shape="square"
fit="fill"
class="lg:h-7 lg:w-auto pb-2 h-5 w-[90px] h-[28px]"
:alt="config.siteName"
:title="config.siteName"
/>
<!-- <span class="text-gray-500 text-2.5" style="line-height: 1rem">云应用开发平台</span>-->
</div>
</nuxt-link>
<nuxt-link v-else to="/">
<text>{{ config?.siteName }}</text>
</nuxt-link>
</div>
<div class="hidden sm:flex">
<el-menu
:default-active="currentIndex"
mode="horizontal"
background-color="transparent"
:collapse="true"
:ellipsis="false"
style="border: none"
>
<template v-for="(item, index) in navigations">
<el-sub-menu v-if="item?.children && item.children.length > 0" :index="`${item.path}`">
<template #title><h3 @click="openSpmUrl(`${item.path}`,item,item.navigationId)">{{ item.title }}</h3></template>
<el-menu-item v-for="(sub,subIndex) in item.children" :index="`${sub.path}`">
<el-space @click="openSpmUrl(`${item.path}`,sub,sub.navigationId)">
<el-avatar :src="sub.icon" shape="square" size="small"></el-avatar>
<span class="font-bold">{{ sub.title }}</span>
</el-space>
</el-menu-item>
</el-sub-menu>
<el-menu-item v-else :index="`${item.path}`"><h3 @click="openSpmUrl(`${item.path}`,item,item.navigationId)">{{ item.title }}</h3></el-menu-item>
</template>
<!-- <el-menu-item v-if="user?.certification">-->
<!-- <el-space @click="loginDeveloperCenterByToken({domain:'developer.websoft.top'})">-->
<!-- <h3>开发者中心</h3>-->
<!-- </el-space>-->
<!-- </el-menu-item>-->
<!-- <el-menu-item v-else>-->
<!-- <el-space @click="openSpmUrl(`https://developer.websoft.top`)">-->
<!-- <h3>开发者中心</h3>-->
<!-- </el-space>-->
<!-- </el-menu-item>-->
</el-menu>
</div>
</div>
<div class="header__right items-center pr-4 xl:pr-0 md:flex hidden">
<el-space class="sm:flex hidden" size="large" v-if="config.showSearchTools">
<el-button v-if="token" @click="loginAdminByToken">控制台</el-button>
<!-- <el-button v-if="token" @click="navigateTo(`/manage`)">控制台</el-button>-->
<!-- <el-button v-if="config.showSearchIcon" circle :icon="ElIconSearch" @click="navigateTo('/search')"></el-button>-->
<!-- <el-button v-if="token" @click="loginAdminByToken">控制台</el-button>-->
<ClientOnly>
<template v-if="token">
<el-dropdown @command="handleCommand">
<el-space class="flex items-center cursor-pointer">
<el-avatar class="cursor-pointer" :src="user?.logo" :size="30" />
<span>{{ user?.tenantName }}</span>
</el-space>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="user" @click="openSpmUrl(`/user`)">账户中心</el-dropdown-item>
<el-dropdown-item command="password" @click="openSpmUrl(`/user/password`)">修改密码</el-dropdown-item>
<el-dropdown-item command="auth" @click="openSpmUrl(`/user/auth`)">实名认证</el-dropdown-item>
<el-dropdown-item command="order" @click="openSpmUrl(`/user/order`)">我的订单</el-dropdown-item>
<!-- <el-dropdown-item divided command="order" @click="loginDeveloperCenterByToken({domain:'developer.websoft.top'})">开发者中心</el-dropdown-item>-->
<el-dropdown-item divided command="logOut" @click="openSpmUrl('/user/logout')">退出登录
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<template v-else>
<el-button type="primary" v-if="!token" @click="navigateTo(`/passport/login`)">登录/注册</el-button>
<!-- <el-button v-if="config.showLoginButton" circle :icon="ElIconUserFilled" @click="goLogin"></el-button>-->
</template>
</ClientOnly>
</el-space>
</div>
<!-- 移动端菜单 -->
<div class="sm:hidden flex xl:p-0 px-8">
<el-dropdown>
<el-space class="el-dropdown-link flex items-center">
<el-avatar v-if="token" class="cursor-pointer" :src="user?.avatar" :size="30"/>
<el-button v-else :icon="ElIconMenu"></el-button>
</el-space>
<template #dropdown>
<el-dropdown-menu>
<template v-for="(item, index) in navigations">
<el-dropdown-item>
<span @click="openSpmUrl(`${item.path}`,item,item.navigationId)">{{ item.title }}</span>
</el-dropdown-item>
</template>
<template v-if="token">
<el-dropdown-item divided @click="loginDeveloperCenterByToken">开发者中心</el-dropdown-item>
<!-- <el-dropdown-item divided command="user" @click="navigateTo(`/user`)">账户中心</el-dropdown-item>-->
<el-dropdown-item divided command="logOut" @click="navigateTo('/user/logout')">退出</el-dropdown-item>
</template>
<el-dropdown-item v-if="!token" divided @click="navigateTo(`/passport/login`)">登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<Passport :visible="showPassport" @done="reload"/>
</header>
<div class="header__height__placeholder"></div>
</el-affix>
</template>
<script setup lang="ts">
// 引入所需的图标
import {
useCompany,
useConfigInfo,
useMenu,
useProductAffix,
useSysDomain,
useToken,
useUser
} from "~/composables/configState";
import {useServerRequest} from "~/composables/useServerRequest";
import type {ApiResult} from "~/api";
import type {User} from "~/api/system/user/model";
import {navigateTo, openSpmUrl} from "#imports";
import type {Company} from "~/api/system/company/model";
import {loginAdminByToken} from "~/utils/common";
const route = useRoute();
// 配置信息
const runtimeConfig = useRuntimeConfig();
const showPassport = ref<boolean>(false);
const token = useToken();
const user = useUser();
const company = useCompany();
const navigations = useMenu();
const config = useConfigInfo();
const affix = useProductAffix();
const sysDomain = useSysDomain();
// 顶部栏初始数
const visibleNumber = ref<number>(6);
config.value.elMenuMaxNumber = 8;
// 当前激活菜单的 index
const currentIndex = ref<string>('/');
function handleCommand(command: string) {
switch (command) {
case 'logOut':
logOut();
break;
default:
navigateTo('/user');
break;
}
}
const onAffix = (index: boolean) => {
affix.value = index;
}
function logOut() {
token.value = ''
localStorage.clear();
navigateTo('/passport/login')
}
function goLogin() {
navigateTo('/passport/login')
}
function handleSelect(key: string, keyPath: any) {
currentIndex.value = key;
navigateTo(`${key}`);
}
const reload = async () => {
const token = localStorage.getItem('token');
// const domain = localStorage.getItem('SysDomain');
// if (domain) {
// sysDomain.value = domain;
// }
// 获取用户信息
if (token && token != '') {
const {data: response} = await useServerRequest<ApiResult<User>>('/auth/user', {baseURL: runtimeConfig.public.apiServer})
if (response.value?.data) {
user.value = response.value?.data;
}
// 获取企业信息
const {data: companyInfo} = await useServerRequest<ApiResult<Company>>('/system/company/profile', {baseURL: runtimeConfig.public.apiServer})
if(companyInfo.value?.data){
company.value = companyInfo.value?.data;
// sysDomain.value = company.value?.domain || undefined;
// user.value.tenantName = company.value?.tenantName;
// localStorage.setItem('SysDomain', `${company.value?.domain}`);
localStorage.setItem('CompanyLogo', `${company.value?.companyLogo}`)
localStorage.setItem('TenantName', `${company.value?.tenantName}`)
}
}
}
reload();
</script>
<style lang="scss">
body {
background-color: #f3f6f8;
}
.el-menu-item:visited, .el-menu-item:hover {
background: none !important; /* 你可以根据需要设置不同的颜色 */
transition: background-color 0.3s linear;
}
.is-active:hover {
border-bottom: none !important;
}
</style>

38
components/Banner.vue Normal file
View File

@@ -0,0 +1,38 @@
<template>
<div class="banner m-auto relative sm:flex">
<template v-if="layout && layout.showBanner">
<el-image :src="layout?.photo || config.subpageBanner" :class="layout?.style" class="sm:h-auto"></el-image>
<div class="banner-bar absolute top-0 w-full sm:flex hidden">
<div class="banner-text py-12 md:w-screen-xl m-auto opacity-90 flex flex-col justify-center">
<div class="keywords my-4 text-3xl">{{ layout?.name }}</div>
<div class="description mb-4 mt-1 text-xl max-w-3xl text-gray-600">{{ layout?.description }}</div>
<div class="buy-btn">
<el-button v-if="layout.demoUrl" @click="openSpmUrl(layout.demoUrl)" type="primary">演示地址</el-button>
<el-button v-if="token && layout.buyUrl" type="warning" @click="openSpmUrl(`https://${sysDomain}/token-login`)">产品控制台</el-button>
<el-button v-if="!token" type="warning" @click="navigateTo(`/passport/login`)">立即开通</el-button>
<el-button v-if="layout.docUrl" @click="openSpmUrl(layout.docUrl)">产品文档</el-button>
</div>
<div class="demo-account mt-3 text-blue-7" v-if="layout?.account">账号密码{{ layout.account }}</div>
</div>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import {useConfigInfo} from "~/composables/configState";
import {openSpmUrl} from "~/utils/common";
const token = useToken();
const sysDomain = useSysDomain();
withDefaults(
defineProps<{
layout?: any;
}>(),
{}
);
const config = useConfigInfo();
</script>

29
components/Breadcrumb.vue Normal file
View File

@@ -0,0 +1,29 @@
<template>
<div class="sm:py-4 sm:px-0 mx-3 py-2">
<el-breadcrumb :separator-icon="ArrowRight">
<el-breadcrumb-item :to="{ path: '/' }">
<el-icon class="cursor-pointer">
<ElIconHouse/>
</el-icon>
</el-breadcrumb-item>
<el-breadcrumb-item v-if="data?.parentName">
<span class="cursor-pointer hover:font-bold" @click="openSpmUrl(`/product`,data,data.categoryId)">{{ data.parentName }}</span>
</el-breadcrumb-item>
<el-breadcrumb-item v-if="data?.categoryName">{{ title ? title : data.categoryName }}</el-breadcrumb-item>
<el-breadcrumb-item v-if="title">{{ title }}</el-breadcrumb-item>
</el-breadcrumb>
</div>
</template>
<script setup lang="ts">
import {ArrowRight} from '@element-plus/icons-vue'
withDefaults(
defineProps<{
data?: any;
title?: string;
}>(),
{}
);
</script>

44
components/CardList.vue Normal file
View File

@@ -0,0 +1,44 @@
<template>
<div class="xl:w-screen-xl sm:flex xl:p-0 p-4 m-auto relative">
<el-row :gutter="24" class="flex">
<template v-for="(item,index) in list" :key="index">
<el-col :span="6" class="mb-5 min-w-xs">
<el-card shadow="hover" :body-style="{ padding: '0px' }" class="hover:bg-gray-50">
<el-image :src="item.image" fit="contain" :lazy="true" class="w-full h-[150px] cursor-pointer" @click="openSpmUrl(`/detail`,item,item.articleId)" />
<div class="flex-1 px-4 py-5 sm:p-6 !p-4">
<p class="text-gray-900 dark:text-white text-base font-semibold flex items-center gap-1.5">
<a class="flex-1 cursor-pointer" @click="openSpmUrl(`/detail`,item,item.articleId)">{{ item.title }}</a>
</p>
<div class="text-[15px] text-gray-500 dark:text-gray-400 mt-1 line-clamp-2 sm:min-h-[45px]">
{{ item.comments }}
</div>
<div class="button-group flex justify-center mt-3">
<el-button class="w-full" :icon="ElIconView">演示</el-button>
<el-button class="w-full" :icon="ElIconShoppingCart">购买</el-button>
</div>
</div>
</el-card>
</el-col>
</template>
</el-row>
</div>
</template>
<script setup lang="ts">
import {useConfigInfo} from "~/composables/configState";
import {openSpmUrl} from "~/utils/common";
const token = useToken();
const sysDomain = useSysDomain();
withDefaults(
defineProps<{
layout?: any;
list?: any[];
}>(),
{}
);
const config = useConfigInfo();
</script>

102
components/CompanyList.vue Normal file
View File

@@ -0,0 +1,102 @@
<template>
<div v-if="title" class="text-center flex flex-col items-center py-12 z-100 relative">
<h2 class="text-3xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-4xl lg:text-5xl">
{{ title }}
</h2>
<div class="sub-title">
<p class="text-gray-500 dark:text-gray-400 py-3">
{{ comments }}
</p>
</div>
</div>
<div class="xl:w-screen-xl sm:flex xl:p-0 p-4 m-auto relative">
<el-row :gutter="24" class="flex">
<template v-for="(item,index) in list" :key="index">
<el-col :span="6" :xs="24" class="mb-5 min-w-xs">
<el-card shadow="hover" :body-style="{ padding: '0px' }" class="hover:bg-gray-50 cursor-pointer" @click="openSpmUrl(`/item`, item,item.companyId,true)">
<el-image
:src="item.image"
:fit="fit" :lazy="true" class="w-full md:h-[150px] h-[199px] cursor-pointer bg-gray-50"/>
<div class="flex-1 px-4 py-5 sm:p-6 !p-4">
<div class="text-gray-700 dark:text-white text-base font-semibold flex flex-col gap-1.5">
<div class="flex-1 text-xl cursor-pointer flex items-center">
{{ item.shortName }}
<el-tag v-if="item.chargingMethod === 0" size="small" type="success" class="text-white ml-2" effect="dark">免费</el-tag>
</div>
</div>
<div class="flex items-center gap-1.5 py-2 text-gray-500 justify-between">
<div class="text-gray-500 line-clamp-2">{{ item.comments }}</div>
</div>
<div class="button-group flex justify-between items-center mt-3">
<el-space class="flex items-end">
<el-avatar size="small" :src="item.companyLogo" />
<span class="text-gray-400 line-clamp-1 pr-2">{{ item.companyName }}</span>
</el-space>
<template v-if="item.isBuy">
<el-button v-if="item.installed" type="primary" @click.stop="loginDeveloperCenterByToken(item)">控制台</el-button>
<el-button v-else type="primary" @click.stop="loginDeveloperCenterByToken(item)">控制台</el-button>
</template>
<template v-else>
<el-button v-if="item.chargingMethod == 0" type="primary" @click.stop="loginDeveloperCenterByToken(item)">控制台</el-button>
<el-button v-else type="warning" @click.stop="openSpmUrl(`/product/create`,item,item.companyId,true)">立即开通
</el-button>
</template>
<!-- <el-button type="warning" v-if="item.price" @click.stop="openSpmUrl(`/product/create`,item,item.companyId,true)">-->
<!-- <div class="flex items-center">-->
<!-- <span>{{ item.price * 0.1 }}</span>-->
<!-- <span v-if="item.chargingMethod == 2">/</span>-->
<!-- <span v-if="item.chargingMethod == 3">/</span>-->
<!-- <span v-if="item.chargingMethod == 4">/</span>-->
<!-- </div>-->
<!-- </el-button>-->
</div>
</div>
</el-card>
</el-col>
</template>
<!-- <div class="more flex justify-center w-full"><el-button>查看更多</el-button></div>-->
</el-row>
</div>
<div v-if="disabled" class="px-1 text-center text-gray-500 min-h-xs">
没有更多了
</div>
</template>
<script setup lang="ts">
import {loginAdminByToken, loginByToken, loginDeveloperCenterByToken, openSpmUrl} from "~/utils/common";
import {useServerRequest} from "~/composables/useServerRequest";
import type {ApiResult, PageResult} from "~/api";
import type {Company, CompanyParam} from "~/api/system/company/model";
const props = withDefaults(
defineProps<{
param?: CompanyParam;
disabled?: boolean;
title?: string;
comments?: string;
fit?: any;
}>(),
{
fit: 'cover'
}
);
const emit = defineEmits<{
(e: 'done'): void;
}>();
const list = ref<Company[]>([]);
// 请求数据
const reload = async () => {
const {data: response} = await useServerRequest<ApiResult<PageResult<Company>>>('/system/company/page', {
params: props.param
})
if (response.value?.data) {
if (response.value?.data.list) {
list.value = response.value?.data.list;
}
}
}
reload();
</script>

180
components/PageBanner.vue Normal file
View File

@@ -0,0 +1,180 @@
<template>
<div :class="form?.style" class="banner m-auto relative sm:flex">
<svg viewBox="0 0 1440 181" fill="none" xmlns="http://www.w3.org/2000/svg"
class="pointer-events-none absolute w-full top-[-2px] transition-all text-green-5 flex-shrink-0 opacity-100 duration-[400ms] opacity-80 -z-10">
<mask id="path-1-inside-1_414_5526" fill="white">
<path d="M0 0H1440V181H0V0Z"></path>
</mask>
<path d="M0 0H1440V181H0V0Z" fill="url(#paint0_linear_414_5526)" fill-opacity="0.22"></path>
<path d="M0 2H1440V-2H0V2Z" fill="url(#paint1_linear_414_5526)" mask="url(#path-1-inside-1_414_5526)"></path>
<defs>
<linearGradient id="paint0_linear_414_5526" x1="720" y1="0" x2="720" y2="181" gradientUnits="userSpaceOnUse">
<stop stop-color="currentColor"></stop>
<stop offset="1" stop-color="currentColor" stop-opacity="0"></stop>
</linearGradient>
<linearGradient id="paint1_linear_414_5526" x1="0" y1="90.5" x2="1440" y2="90.5" gradientUnits="userSpaceOnUse">
<stop stop-color="currentColor" stop-opacity="0"></stop>
<stop offset="0.395" stop-color="currentColor"></stop>
<stop offset="1" stop-color="currentColor" stop-opacity="0"></stop>
</linearGradient>
</defs>
</svg>
<div class="md:w-screen-xl m-auto xl:p-0 xl:px-0 px-4">
<!-- <Breadcrumb :title="form?.categoryName" />-->
<div class="py-8 sm:py-16" _path="/templates" _dir="" _draft="false" _partial="false" _locale=""
_id="content:4.templates.yml" _type="yaml" _source="content" _file="4.templates.yml" _stem="4.templates"
_extension="yml">
<div class="gap-8 sm:gap-y-16 grid lg:grid-cols-2 lg:items-center">
<div class="flex">
<el-avatar v-if="avatar" :src="avatar" :size="120"></el-avatar>
<div class="title-bar">
<h1 class="text-3xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-4xl lg:text-5xl">
<span>{{ title }}</span>
</h1>
<div class="mt-4 text-lg text-gray-500 dark:text-gray-400">
{{ desc }}
</div>
<div class="flex justify-between w-full items-center mt-4" v-if="showSearch">
<el-space>
<div class="w-[500px] pr-10">
<el-input
v-model="where.keywords"
placeholder="搜索"
:prefix-icon="Search"
@keydown.enter="handleClick"
>
<template #append>
<el-button size="large" type="primary" @click="handleClick">搜索</el-button>
</template>
</el-input>
</div>
</el-space>
<el-space>
<el-button size="large">我的收藏</el-button>
<el-button size="large">热门推荐</el-button>
</el-space>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 密码可见 -->
<el-dialog
v-model="dialogVisible"
title="请输入查看密码"
:show-close="false"
:close-on-click-modal="false"
width="400"
>
<el-input type="password" v-model="password2" show-password placeholder="请输入查看密码">请输入查看密码</el-input>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="checkPassword">
确定
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import {useServerRequest} from "~/composables/useServerRequest";
import type {ApiResult} from "~/api";
import type {Navigation} from "~/api/cms/navigation/model";
import {ref} from 'vue'
import {Search} from '@element-plus/icons-vue'
import type {ProductParam} from "~/api/oa/product/model";
withDefaults(
defineProps<{
form?: any;
layout?: any;
title?: string;
desc?: string;
buyUrl?: string;
avatar?: string;
showSearch?: boolean;
}>(),
{
title: 'PageName',
desc: 'Description',
demoUrl: '/product',
buyUrl: '/buy'
}
);
const emit = defineEmits<{
(e: 'done', show: boolean): void;
(e: 'search', where: ProductParam): void;
}>();
const route = useRoute();
const token = useToken();
const form = ref<Navigation>();
const layout = ref<any>();
const password2 = ref('');
// 密码弹窗
const dialogVisible = ref(false)
// 搜索表单
const where = reactive<ProductParam>({
keywords: ''
});
// 验证密码
const checkPassword = async () => {
const {data: response} = await useServerRequest<ApiResult<unknown>>('/cms/cms-navigation/checkNavigationPassword', {
query: {
password: form.value?.password,
password2: password2.value
}
})
if (response.value?.code === 0) {
dialogVisible.value = false;
emit('done', true);
console.log(response.value.message);
} else {
ElMessage.error(response.value?.message);
}
}
// 搜索结果
const handleClick = async () => {
emit('search',where);
}
const reload = async () => {
// 校验密码
const {data: nav} = await useServerRequest<ApiResult<Navigation>>('/cms/cms-navigation/getNavigationByPath', {
query: {
path: route.path
}
})
if (nav.value?.data) {
form.value = nav.value?.data;
console.log('PageBanner.vue => ', form.value)
// 页面布局
if (form.value?.layout) {
layout.value = JSON.parse(form.value?.layout)
}
// 允许直接访问
if (form.value?.permission === 0) {
emit('done', true);
}
// 要求登录可见
if (form.value?.permission === 1) {
navigateTo(`/passport/login`);
if (token && token.value.length > 0) {
emit('done', true);
}
}
// 要求密码可见
if (form.value?.permission === 2) {
dialogVisible.value = true;
}
}
}
reload();
</script>

View File

@@ -0,0 +1,30 @@
<template>
<div class="container md:w-screen-xl m-auto">
<div class="flex flex-col" v-if="!layout?.showLayout">
<Breadcrumb :data="form" />
<div :class="layout?.style" class="page-main w-full bg-white rounded-lg">
<div class="p-4 leading-7" v-html="form?.design?.content">
</div>
</div>
</div>
</div>
<!-- 空白页 -->
<div class="mt-[60px]" v-if="!layout">
<el-empty description="404 页面不存在"></el-empty>
</div>
</template>
<script setup lang="ts">
import Breadcrumb from "~/components/Breadcrumb.vue";
import type {Navigation} from "~/api/cms/navigation/model";
withDefaults(
defineProps<{
layout?: any;
form?: Navigation;
}>(),
{}
);
</script>

51
components/Passport.vue Normal file
View File

@@ -0,0 +1,51 @@
<template>
<el-dialog v-model="showLogin" width="500" center>
<el-segmented v-model="loginType" :options="options" block />
<el-form :model="form" label-width="auto" style="max-width: 600px" class="p-4">
<el-form-item label="登录账号">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="登录密码">
<el-input v-model="form.name" type="password" />
</el-form-item>
<el-form-item label="记住密码">
<el-switch v-model="form.delivery" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button >取消</el-button>
<el-button type="primary" >
确定
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import {useShowLogin, useToken} from "~/composables/configState";
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
}>();
const form = reactive({
name: '',
region: '',
date1: '',
date2: '',
delivery: false,
type: [],
resource: '',
desc: '',
})
const showLogin = useShowLogin();
const loginType = ref('账号登录');
const options = [
'账号登录',
'扫码登录',
]
const token = useToken();
</script>

View File

@@ -0,0 +1,94 @@
<template>
<div class="text-center flex flex-col items-center py-12 z-100 relative">
<h2 class="text-3xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-4xl lg:text-5xl">
{{ title }}
</h2>
<div class="sub-title">
<p class="text-gray-500 dark:text-gray-400 py-3">
{{ comments }}
</p>
</div>
</div>
<div class="xl:w-screen-xl sm:flex xl:p-0 p-4 m-auto relative">
<el-row :gutter="24" class="flex">
<template v-for="(item,index) in list" :key="index">
<el-col :span="6" :xs="24" class="mb-5 min-w-xs">
<el-card shadow="hover" :body-style="{ padding: '0px' }" class="hover:bg-gray-50 cursor-pointer" @click="openSpmUrl(`/item`, item,item.productId,true)">
<el-image
:src="item.image"
:fit="fit" :lazy="true" class="w-full md:h-[150px] h-[199px] cursor-pointer bg-gray-50"/>
<div class="flex-1 px-4 py-5 sm:p-6 !p-4">
<div class="text-gray-700 dark:text-white text-base font-semibold flex flex-col gap-1.5">
<div class="flex-1 text-xl cursor-pointer flex items-center">
{{ item.title }}
<el-tag v-if="item.tag == '1'" size="small" type="success" class="text-white ml-2" effect="dark">免费</el-tag>
<el-tag v-if="item.tag == '2'" size="small" type="success" class="text-white ml-2" effect="dark">开源</el-tag>
<el-tag v-if="item.tag == '3'" size="small" type="danger" class="text-white ml-2" effect="dark">付费</el-tag>
</div>
</div>
<div class="flex items-center gap-1.5 py-2 text-gray-500 justify-between">
<div class="text-gray-500 line-clamp-3">{{ item.comments }}</div>
</div>
<div class="button-group flex justify-between items-center mt-3">
<div class="flex items-end">
<span class="text-red-500 text-xl">{{ item.price }}</span>
<span v-if="item.durationMethod == 1" class="px-1 pb-1 text-xs text-gray-400">/</span>
<span v-if="item.durationMethod == 2" class="px-1 pb-1 text-xs text-gray-400">/</span>
<span v-if="item.durationMethod == 3" class="px-1 pb-1 text-xs text-gray-400">/</span>
</div>
{{ item.chargingMethod }}
<el-button type="warning" @click.stop="openSpmUrl(`/product/create`,item,item.productId,true)">立即开通
</el-button>
</div>
</div>
</el-card>
</el-col>
</template>
<!-- <div class="more flex justify-center w-full"><el-button>查看更多</el-button></div>-->
</el-row>
</div>
<div v-if="disabled" class="px-1 text-center text-gray-500 min-h-xs">
没有更多了
</div>
</template>
<script setup lang="ts">
import {openSpmUrl} from "~/utils/common";
import {useServerRequest} from "~/composables/useServerRequest";
import type {ApiResult, PageResult} from "~/api";
import type {Product, ProductParam} from "~/api/oa/product/model";
const props = withDefaults(
defineProps<{
param?: ProductParam;
disabled?: boolean;
title?: string;
comments?: string;
fit?: any;
}>(),
{
title: '卡片标题',
comments: '卡片描述',
fit: 'cover'
}
);
const emit = defineEmits<{
(e: 'done'): void;
}>();
const list = ref<Product[]>([]);
// 请求数据
const reload = async () => {
const {data: response} = await useServerRequest<ApiResult<PageResult<Product>>>('/cms/cms-product/page', {
params: props.param
})
if (response.value?.data) {
if (response.value?.data.list) {
list.value = response.value?.data.list;
}
}
}
reload();
</script>

View File

@@ -0,0 +1,42 @@
<template>
<div class="shopinfo px-3 sm:px-0">
<div class="bg-white p-3 mt-3 flex items-center justify-between rounded-xl">
<div class="left flex gap-xs items-center">
<el-avatar shape="square" :src="data?.image" :size="55" />
<div class="shop-name flex flex-col">
<div class="shop-name cursor-pointer">
<div class="flex items-center">
<text class="text-xl">{{ data?.merchantName }}</text>
<el-tag type="success" size="small" class="ml-2">官方</el-tag>
</div>
</div>
<el-rate
v-model="rate"
show-score
text-color="#ff9900"
score-template="{value}">
</el-rate>
</div>
</div>
<el-button>进入店铺</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import type {Navigation} from "~/api/cms/navigation/model";
import type {Merchant} from "~/api/shop/merchant/model";
withDefaults(
defineProps<{
data?: Merchant;
}>(),
{}
);
const rate = ref(5);
</script>
<style scoped lang="scss">
</style>

View File

@@ -0,0 +1,24 @@
<template>
<el-card class="m-5 w-screen-sm mt-[60px] m-auto">
<!-- 异常状态 -->
<el-result
:icon="website.statusIcon || 'info'"
:title="`${website.statusName || '404'}`"
:sub-title="website.statusText || '链接失败请检查您的网络或与网站管理员联系'"
>
<template #extra>
<el-button type="primary" v-if="website.statusUrl" @click="navigateTo(`${website.statusUrl}`)">{{ website.statusBtnText }}</el-button>
</template>
</el-result>
</el-card>
</template>
<script setup lang="ts">
import {useWebsite} from "~/composables/configState";
const website = useWebsite()
const navigateTo = (url: string) => {
window.location.href = url;
}
</script>

61
components/Upload.vue Normal file
View File

@@ -0,0 +1,61 @@
<template>
<el-upload action="#" list-type="picture-card" :auto-upload="false">
<el-icon><Plus /></el-icon>
<template #file="{ file }">
<div>
<img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
<span class="el-upload-list__item-actions">
<span
class="el-upload-list__item-preview"
@click="handlePictureCardPreview(file)"
>
<el-icon><zoom-in /></el-icon>
</span>
<span
v-if="!disabled"
class="el-upload-list__item-delete"
@click="handleDownload(file)"
>
<el-icon><Download /></el-icon>
</span>
<span
v-if="!disabled"
class="el-upload-list__item-delete"
@click="handleRemove(file)"
>
<el-icon><Delete /></el-icon>
</span>
</span>
</div>
</template>
</el-upload>
<el-dialog v-model="dialogVisible">
<img :src="dialogImageUrl" alt="Preview Image" />
</el-dialog>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { Delete, Download, Plus, ZoomIn } from '@element-plus/icons-vue'
import type { UploadFile } from 'element-plus'
const dialogImageUrl = ref('')
const dialogVisible = ref(false)
const disabled = ref(false)
const handleRemove = (file: UploadFile) => {
console.log(file)
}
const handlePictureCardPreview = (file: UploadFile) => {
dialogImageUrl.value = file.url!
dialogVisible.value = true
}
const handleDownload = (file: UploadFile) => {
console.log(file)
}
</script>

225
components/UserCard.vue Normal file
View File

@@ -0,0 +1,225 @@
<template>
<div :class="form?.style" class="banner m-auto relative sm:flex">
<svg viewBox="0 0 1440 181" fill="none" xmlns="http://www.w3.org/2000/svg"
class="pointer-events-none absolute w-full top-[-2px] transition-all text-green-5 flex-shrink-0 opacity-100 duration-[400ms] opacity-80 -z-10">
<mask id="path-1-inside-1_414_5526" fill="white">
<path d="M0 0H1440V181H0V0Z"></path>
</mask>
<path d="M0 0H1440V181H0V0Z" fill="url(#paint0_linear_414_5526)" fill-opacity="0.22"></path>
<path d="M0 2H1440V-2H0V2Z" fill="url(#paint1_linear_414_5526)" mask="url(#path-1-inside-1_414_5526)"></path>
<defs>
<linearGradient id="paint0_linear_414_5526" x1="720" y1="0" x2="720" y2="181" gradientUnits="userSpaceOnUse">
<stop stop-color="currentColor"></stop>
<stop offset="1" stop-color="currentColor" stop-opacity="0"></stop>
</linearGradient>
<linearGradient id="paint1_linear_414_5526" x1="0" y1="90.5" x2="1440" y2="90.5" gradientUnits="userSpaceOnUse">
<stop stop-color="currentColor" stop-opacity="0"></stop>
<stop offset="0.395" stop-color="currentColor"></stop>
<stop offset="1" stop-color="currentColor" stop-opacity="0"></stop>
</linearGradient>
</defs>
</svg>
<div class="md:w-screen-xl m-auto xl:p-0 px-4">
<div class="py-8 sm:py-16" _path="/templates" _dir="" _draft="false" _partial="false" _locale=""
_id="content:4.templates.yml" _type="yaml" _source="content" _file="4.templates.yml" _stem="4.templates"
_extension="yml">
<div class="gap-8 sm:gap-y-16 grid lg:grid-cols-2 lg:items-center">
<el-space class="flex">
<div class="mr-4" v-if="company.companyLogo">
<el-avatar :src="company.companyLogo" :size="150"></el-avatar>
</div>
<div class="title-bar">
<h1 class="text-3xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-4xl lg:text-5xl">
<span>{{ company.shortName }}</span>
</h1>
<div class="mt-4 text-lg text-gray-500 dark:text-gray-400">
{{ company?.comments || desc }}
</div>
<el-space class="mt-4">
<el-button
:icon="ElIconPlus"
@click="openSpmUrl(`http://git.gxwebsoft.com/gxwebsoft/websoft-cms.git`, {},0,true)"
>
关注
</el-button>
<!-- <el-tree-select-->
<!-- v-model="where.categoryId"-->
<!-- v-if="navigation?.length > 0"-->
<!-- :data="navigation"-->
<!-- placeholder="选择分类"-->
<!-- :render-after-expand="false"-->
<!-- style="width: 240px"-->
<!-- />-->
<div class="w-[300px]">
<el-input
v-model="where.keywords"
placeholder="搜索"
:prefix-icon="Search"
@keydown.enter="reload"
>
<template #append>
<el-button type="primary" @click="reload">搜索</el-button>
</template>
</el-input>
</div>
</el-space>
</div>
</el-space>
</div>
</div>
</div>
<!-- 密码可见 -->
<el-dialog
v-model="dialogVisible"
title="请输入查看密码"
:show-close="false"
:close-on-click-modal="false"
width="400"
>
<el-input type="password" v-model="password2" show-password placeholder="请输入查看密码">请输入查看密码</el-input>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="checkPassword">
确定
</el-button>
</div>
</template>
</el-dialog>
</div>
<!-- <el-space class="mt-4">-->
<!-- <el-button-->
<!-- :icon="ElIconBottom"-->
<!-- @click="openSpmUrl(`http://git.gxwebsoft.com/gxwebsoft/websoft-cms.git`, {},0,true)"-->
<!-- >-->
<!-- 关注-->
<!-- </el-button>-->
<!-- <el-button-->
<!-- :icon="ElIconMemo"-->
<!-- @click="openSpmUrl(`/docs`)"-->
<!-- >-->
<!-- 选择分类-->
<!-- </el-button>-->
<!-- <div class="w-[500px]">-->
<!-- <el-input-->
<!-- v-model="where.keywords"-->
<!-- placeholder="搜索"-->
<!-- :prefix-icon="Search"-->
<!-- @keydown.enter="reload"-->
<!-- >-->
<!-- <template #append>-->
<!-- <el-button type="primary" @click="reload">搜索</el-button>-->
<!-- </template>-->
<!-- </el-input>-->
<!-- </div>-->
<!-- </el-space>-->
</template>
<script setup lang="ts">
import {Search} from '@element-plus/icons-vue'
import {openSpmUrl} from "~/utils/common";
import {useServerRequest} from "~/composables/useServerRequest";
import type {ApiResult} from "~/api";
import type {Navigation} from "~/api/cms/navigation/model";
import {ref} from 'vue'
import type {CompanyParam} from "~/api/system/company/model";
withDefaults(
defineProps<{
form?: any;
layout?: any;
title?: string;
desc?: string;
buyUrl?: string;
avatar?: string;
}>(),
{
title: 'PageName',
desc: 'Description',
demoUrl: '/product',
buyUrl: '/buy'
}
);
const emit = defineEmits<{
(e: 'done', show: boolean): void;
}>();
const route = useRoute();
const token = useToken();
const form = ref<Navigation>();
const layout = ref<any>();
const password2 = ref('');
// 密码弹窗
const dialogVisible = ref(false)
const company = useCompany();
const navigation = ref<Navigation[]>()
// 搜索表单
const where = reactive<CompanyParam>({
keywords: ''
});
// 验证密码
const checkPassword = async () => {
const {data: response} = await useServerRequest<ApiResult<unknown>>('/cms/cms-navigation/checkNavigationPassword', {
query: {
password: form.value?.password,
password2: password2.value
}
})
if (response.value?.code === 0) {
dialogVisible.value = false;
emit('done', true);
console.log(response.value.message);
} else {
ElMessage.error(response.value?.message);
}
}
const reload = async () => {
useClientRequest<ApiResult<Navigation[]>>('/cms/cms-navigation', {
params: {
userId: route.params.userId
}
}).then(res => {
navigation.value = res?.data?.map(d => {
return {
label: d.title,
value: d.navigationId,
children: null
}
});
console.log(navigation.value)
})
// 校验密码
const {data: nav} = await useServerRequest<ApiResult<Navigation>>('/cms/cms-navigation/getNavigationByPath', {
query: {
path: route.path
}
})
if (nav.value?.data) {
form.value = nav.value?.data;
console.log('PageBanner.vue => ', form.value)
// 页面布局
if (form.value?.layout) {
layout.value = JSON.parse(form.value?.layout)
}
// 允许直接访问
if (form.value?.permission === 0) {
emit('done', true);
}
// 要求登录可见
if (form.value?.permission === 1) {
navigateTo(`/passport/login`);
if (token && token.value.length > 0) {
emit('done', true);
}
}
// 要求密码可见
if (form.value?.permission === 2) {
dialogVisible.value = true;
}
}
}
reload();
</script>