项目初始化

This commit is contained in:
2026-03-06 00:29:28 +08:00
commit 9ed4c1bc58
577 changed files with 57404 additions and 0 deletions

View File

@@ -0,0 +1,610 @@
<template>
<div>
<el-dialog v-model="state.dialog.goodsInfo" class="goods-info-dialog" :title="t('module.detailed information')" width="60%">
<el-scrollbar v-loading="state.loading.goodsInfo" :key="state.goodsInfo.uid" :height="500">
<div class="goods-info">
<div class="goods-images">
<el-carousel height="300" v-if="state.goodsInfo.images" indicator-position="outside">
<el-carousel-item class="goods-image-item" v-for="(image, idx) in state.goodsInfo.images" :key="idx">
<el-image fit="contain" :preview-src-list="state.goodsInfo.images" :preview-teleported="true" :src="image"></el-image>
</el-carousel-item>
</el-carousel>
</div>
<div class="goods-basic">
<h4 class="goods-basic-title">{{ state.goodsInfo.title }}</h4>
<div class="goods-tag">
<el-tag v-for="(tag, idx) in state.goodsInfo.tags" :key="idx" :type="tag.type ? tag.type : 'primary'">
{{ tag.name }}
</el-tag>
</div>
<div class="basic-item">
<div class="basic-item-title">{{ t('module.Price') }}</div>
<div class="basic-item-price">
{{
typeof state.goodsInfo.currency_select != 'undefined'
? currency(state.goodsInfo.present_price, state.goodsInfo.currency_select)
: '-'
}}
</div>
</div>
<div class="basic-item">
<div class="basic-item-title">{{ t('module.Last updated') }}</div>
<div class="basic-item-content">{{ state.goodsInfo.updatetime ? timeFormat(state.goodsInfo.updatetime) : '-' }}</div>
</div>
<div class="basic-item">
<div class="basic-item-title">{{ t('module.Published on') }}</div>
<div class="basic-item-content">{{ state.goodsInfo.createtime ? timeFormat(state.goodsInfo.createtime) : '-' }}</div>
</div>
<div v-if="!installButtonState.stateSwitch.includes(state.goodsInfo.state)" class="basic-item">
<div class="basic-item-title">{{ t('module.amount of downloads') }}</div>
<div class="basic-item-content">{{ state.goodsInfo.downloads ? state.goodsInfo.downloads : '-' }}</div>
</div>
<div class="basic-item">
<div class="basic-item-title">{{ t('module.Module classification') }}</div>
<div class="basic-item-content">{{ state.goodsInfo.category ? state.goodsInfo.category.name : '-' }}</div>
</div>
<div class="basic-item">
<div class="basic-item-title">{{ t('module.Module documentation') }}</div>
<div class="basic-item-content">
<el-link
type="primary"
class="basic-item-link"
v-if="state.goodsInfo.docs"
target="_blank"
:href="`https://doc.buildadmin.com/md/${state.goodsInfo.docs.name ? state.goodsInfo.docs.name : state.goodsInfo.docs.id}`"
rel="noopener noreferrer"
>
{{ t('module.Click to access') }}
</el-link>
<span v-else>-</span>
</div>
</div>
<div class="basic-item">
<div class="basic-item-title">{{ t('module.Developer Homepage') }}</div>
<div class="basic-item-content">
<el-link
type="primary"
class="basic-item-link"
v-if="state.goodsInfo.author_url"
target="_blank"
:href="state.goodsInfo.author_url"
rel="noopener noreferrer"
>
{{ t('module.Click to access') }}
</el-link>
<span v-else>-</span>
</div>
</div>
<div v-if="installButtonState.stateSwitch.includes(state.goodsInfo.state)" class="basic-item">
<div class="basic-item-title">{{ t('module.Module status') }}</div>
<div class="basic-item-content">
<el-switch
@change="onChangeState"
:loading="state.loading.common"
:disabled="state.loading.common"
v-model="state.goodsInfo.enable"
/>
</div>
</div>
<div class="basic-buttons">
<el-dropdown
v-if="
(!state.goodsInfo.purchased || installButtonState.InstallNow.includes(state.goodsInfo.state)) &&
state.goodsInfo.demo &&
state.goodsInfo.demo.length > 0
"
>
<el-button class="basic-button-demo" type="primary">
<span class="basic-button-dropdown-span">{{ t('module.View demo') }}</span>
<Icon color="#ffffff" size="16" name="el-icon-ArrowDown" />
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="(demo, idx) in state.goodsInfo.demo"
:key="idx"
@click="openDemo(demo.link, demo.image ? false : true)"
class="basic-button-dropdown-item"
>
<el-popover
placement="right"
:title="t('module.Code scanning Preview')"
trigger="hover"
:disabled="demo.image ? false : true"
:width="174"
>
<template #reference>
<div class="demo-item-title">
<Icon :name="demo.icon" size="14" color="var(--el-color-primary)" />{{ demo.title }}
</div>
</template>
<div class="demo-image">
<img :src="demo.image" alt="" />
</div>
</el-popover>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button
v-if="
!state.goodsInfo.purchased &&
installButtonState.buy.includes(state.goodsInfo.state) &&
state.goodsInfo.type == 'online'
"
@click="onBuy(false)"
v-blur
class="basic-button-item"
type="danger"
>
{{ t('module.Buy now') }}
</el-button>
<el-button
v-if="
(state.goodsInfo.state == moduleInstallState.UNINSTALLED && state.goodsInfo.purchased) ||
state.goodsInfo.state == moduleInstallState.WAIT_INSTALL
"
@click="
onPreInstallModule(
state.goodsInfo.uid,
state.goodsInfo.purchased,
state.goodsInfo.state == moduleInstallState.WAIT_INSTALL ? false : true
)
"
:loading="state.loading.common"
v-blur
class="basic-button-item"
type="success"
>
{{ t('module.Install now') }}
</el-button>
<el-button
v-if="installButtonState.continueInstallation.includes(state.goodsInfo.state)"
@click="onPreInstallModule(state.goodsInfo.uid, state.goodsInfo.purchased, false)"
:loading="state.loading.common"
v-blur
class="basic-button-item"
type="success"
>
{{ t('module.continue installation') }}
</el-button>
<el-button
v-if="installButtonState.alreadyInstalled.includes(state.goodsInfo.state)"
v-blur
:disabled="true"
class="basic-button-item"
>
{{ t('module.installed') }} v{{ state.goodsInfo.version }}
</el-button>
<el-button
v-if="state.goodsInfo.type == 'local' && !installButtonState.alreadyInstalled.includes(state.goodsInfo.state)"
v-blur
:disabled="true"
class="basic-button-item"
>
{{ t('module.Local module') }} v{{ state.goodsInfo.version }}
</el-button>
<el-button
v-if="state.goodsInfo.new_version && installButtonState.updateButton.includes(state.goodsInfo.state)"
@click="onUpdate(state.goodsInfo.uid, state.goodsInfo.purchased)"
v-loading="state.loading.common"
v-blur
class="basic-button-item"
type="success"
>
{{ t('module.to update') }}
</el-button>
<el-button
v-if="installButtonState.stateSwitch.includes(state.goodsInfo.state)"
v-loading="state.loading.common"
@click="unInstall(state.goodsInfo.uid)"
v-blur
class="basic-button-item"
type="danger"
>
{{ t('module.uninstall') }}
</el-button>
</div>
</div>
<div v-if="!isEmpty(state.goodsInfo.developer)" class="goods-developer">
<div class="developer-header">
<el-avatar :size="60" :src="state.goodsInfo.developer.avatar" />
<div class="developer-name">
<h3 class="developer-nickname">{{ state.goodsInfo.developer.nickname }}</h3>
<div class="developer-group">
{{ state.goodsInfo.developer.group ? state.goodsInfo.developer.group : '-' }}
</div>
</div>
</div>
<div v-if="state.goodsInfo.qq" class="developer-contact">
<h4 class="developer-info-title">{{ t('module.Contact developer') }}</h4>
<div class="contact-item">
<a
rel="noopener noreferrer"
target="_blank"
:href="'http://wpa.qq.com/msgrd?v=3&uin=' + state.goodsInfo.qq + '&site=qq&menu=yes'"
>
<span>QQ{{ state.goodsInfo.qq }}</span>
</a>
</div>
</div>
<div class="developer-recommend">
<h4 class="developer-info-title">{{ t('module.Other works of developers') }}</h4>
<div v-if="state.goodsInfo.developer.goods.length > 0" class="recommend-goods">
<div
v-for="(goods_item, idx) in state.goodsInfo.developer.goods"
:key="idx"
@click="showInfo(goods_item.uid)"
class="recommend-goods-item"
>
<el-image fit="contain" class="recommend-goods-logo" :src="goods_item.logo"> </el-image>
<div class="recommend-goods-title">{{ goods_item.title }}</div>
</div>
</div>
<div v-else class="data-empty">{{ t('module.There are no more works') }}</div>
</div>
</div>
</div>
<div class="goods-detail ba-markdown" v-html="state.goodsInfo.detail_editor"></div>
<div class="goods-version">
<h1>{{ t('module.Update Log') }}</h1>
<div class="version-timeline" v-if="state.goodsInfo.version_log">
<el-timeline>
<el-timeline-item
v-for="(version, idx) in state.goodsInfo.version_log"
:key="idx"
:timestamp="timeFormat(version.createtime)"
placement="top"
:color="idx == 0 ? 'var(--el-color-success)' : ''"
>
<el-card class="version-card" shadow="hover">
<template #header>
<div class="version-card-header">
<h2>{{ version.title }}</h2>
<span class="version-short-describe">{{ version.short_describe }}</span>
</div>
</template>
<div
class="version-detail ba-markdown"
v-html="version.describe ? version.describe : t('module.No detailed update log')"
></div>
</el-card>
</el-timeline-item>
</el-timeline>
</div>
<div v-else class="empty-update-log">{{ $t('module.No detailed update log') }}</div>
</div>
</el-scrollbar>
</el-dialog>
<Buy />
<Pay />
</div>
</template>
<script setup lang="ts">
import { ElMessageBox } from 'element-plus'
import { isEmpty } from 'lodash-es'
import { useI18n } from 'vue-i18n'
import { currency, onBuy, onDisable, onEnable, onPreInstallModule, onRefreshTableData, showInfo } from '../index'
import { state } from '../store'
import { moduleInstallState } from '../types'
import Buy from './buy.vue'
import Pay from './pay.vue'
import { getInstallState, postUninstall } from '/@/api/backend/module'
import { useBaAccount } from '/@/stores/baAccount'
import { timeFormat } from '/@/utils/common'
const installButtonState = {
InstallNow: [moduleInstallState.UNINSTALLED, moduleInstallState.WAIT_INSTALL],
continueInstallation: [moduleInstallState.CONFLICT_PENDING, moduleInstallState.DEPENDENT_WAIT_INSTALL],
alreadyInstalled: [moduleInstallState.INSTALLED],
stateSwitch: [
moduleInstallState.INSTALLED,
moduleInstallState.CONFLICT_PENDING,
moduleInstallState.DEPENDENT_WAIT_INSTALL,
moduleInstallState.DISABLE,
],
updateButton: [moduleInstallState.WAIT_INSTALL, moduleInstallState.INSTALLED, moduleInstallState.DISABLE],
buy: [moduleInstallState.UNINSTALLED],
}
const { t } = useI18n()
const openDemo = (url: string, open: boolean) => {
if (!open || !url) return
window.open(url)
}
const onChangeState = () => {
if (state.goodsInfo.enable) {
onEnable(state.goodsInfo.uid)
} else {
state.common.disableParams = {
uid: state.goodsInfo.uid,
state: 0,
}
onDisable()
}
}
const unInstall = (uid: string) => {
state.loading.common = true
postUninstall(uid)
.then(() => {
onRefreshTableData()
state.dialog.goodsInfo = false
})
.finally(() => {
state.loading.common = false
})
}
const onUpdate = (uid: string, order: number) => {
// 无有效订单
if (!order) {
ElMessageBox.confirm(t('module.No module purchase order was found'), t('Reminder'), {
confirmButtonText: t('Confirm'),
cancelButtonText: t('Cancel'),
type: 'warning',
})
.then(() => {
onBuy(true)
})
.catch(() => {})
return
}
// 未登录
const baAccount = useBaAccount()
if (!baAccount.token) {
state.dialog.baAccount = true
return
}
state.loading.common = true
getInstallState(uid)
.then((res) => {
if (res.data.state == moduleInstallState.DISABLE) {
onPreInstallModule(uid, order, true, true)
} else {
ElMessageBox.confirm(t('module.You need to disable this module before updating Do you want to disable it now?'), t('Reminder'), {
confirmButtonText: t('module.Disable and update'),
cancelButtonText: t('Cancel'),
type: 'warning',
})
.then(() => {
state.common.disableParams = {
uid: uid,
state: 0,
update: 1,
}
onDisable()
})
.catch(() => {})
}
})
.finally(() => {
state.loading.common = false
})
}
</script>
<style scoped lang="scss">
:deep(.goods-info-dialog) .el-dialog__body {
padding: 0px 20px;
}
.demo-image,
.demo-image img {
width: 150px;
height: 150px;
}
.demo-item-title {
display: flex;
align-items: center;
.icon {
margin-right: 6px;
}
}
.goods-info {
display: flex;
position: relative;
.goods-images {
max-width: 41%;
width: 300px;
.goods-image-item {
display: flex;
align-items: center;
justify-content: center;
}
:deep(.el-carousel__indicators) {
line-height: 10px;
.el-carousel__indicator {
padding: 0 var(--el-carousel-indicator-padding-horizontal);
}
}
}
.goods-basic {
position: relative;
.goods-basic-title {
padding-bottom: 20px;
}
flex: 1;
padding: 0 10px;
.basic-item {
display: flex;
align-items: center;
padding: 4px 0;
.basic-item-title {
font-size: var(--el-font-size-base);
color: var(--el-text-color-secondary);
width: 80px;
}
.basic-item-price {
font-size: 16px;
color: var(--el-color-danger);
}
.basic-item-content {
font-size: var(--el-font-size-base);
color: var(--el-text-color-regular);
}
}
.basic-button-dropdown-span {
padding-right: 6px;
}
.basic-buttons {
padding-top: 6px;
}
.basic-button-demo {
margin-right: 10px;
}
}
.goods-developer {
width: 20%;
border-left: 1px solid var(--ba-bg-color);
padding: 10px;
position: absolute;
right: 0;
.developer-header {
display: flex;
align-items: center;
justify-content: center;
.developer-name {
padding-left: 10px;
flex: 1;
.developer-group {
padding-top: 5px;
font-size: var(--el-font-size-extra-small);
color: var(--el-text-color-secondary);
}
}
}
.developer-info-title {
color: var(--el-text-color-secondary);
padding-top: 15px;
line-height: 20px;
text-align: center;
}
.contact-item {
cursor: pointer;
padding-left: 10px;
line-height: 30px;
text-align: center;
a {
color: var(--el-color-primary);
text-decoration: none;
}
}
.recommend-goods-item {
display: flex;
align-items: center;
margin: 4px 0;
cursor: pointer;
padding: 6px;
&:hover {
background-color: var(--ba-bg-color);
}
.recommend-goods-logo {
width: 42px;
border-radius: var(--el-border-radius-base);
}
.recommend-goods-title {
flex: 1;
margin-left: 6px;
font-size: var(--el-font-size-small);
display: block;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
line-height: 15px;
height: 28px;
}
}
.developer-recommend {
.data-empty {
font-size: var(--el-font-size-extra-small);
color: var(--el-text-color-secondary);
text-align: center;
padding: 6px;
}
}
}
.el-carousel__item:nth-child(2n) {
background-color: #99a9bf;
}
.basic-item-link {
font-size: var(--el-font-size-small);
}
}
.basic-button-item {
--el-loading-spinner-size: 22px;
}
.goods-detail {
width: 80%;
}
.goods-version {
width: 80%;
h1 {
margin: 1.4em 0 0.8em;
font-weight: 700;
font-size: var(--el-font-size-large);
text-transform: uppercase;
color: var(--el-color-primary);
}
.version-timeline {
padding-left: 2px;
:deep(.el-card__body) {
padding: 10px 20px 20px 20px;
}
}
.version-card {
border: 1px solid var(--el-color-info-light-9);
}
.version-card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
}
.empty-update-log {
display: flex;
justify-content: center;
color: var(--el-color-info);
}
/* 商品详情弹窗-s */
@media screen and (max-width: 1440px) {
:deep(.goods-info-dialog) {
--el-dialog-width: 65% !important;
}
}
@media screen and (max-width: 1280px) {
:deep(.goods-info-dialog) {
--el-dialog-width: 80% !important;
}
}
@media screen and (max-width: 1024px) {
:deep(.goods-info-dialog) {
--el-dialog-width: 92% !important;
}
}
/* 商品详情弹窗-e */
@media screen and (max-width: 865px) {
.goods-info .goods-developer {
display: none;
}
}
@media screen and (max-width: 540px) {
.goods-info {
flex-wrap: wrap;
.goods-images {
max-width: 100%;
width: 100%;
}
}
.goods-detail {
padding-top: 15px;
}
}
</style>