feat: add dashboard and virtual pay enhancements

This commit is contained in:
王鹏
2026-07-29 14:34:54 +08:00
parent 2250f744d1
commit 349f1c0374
55 changed files with 3085 additions and 213 deletions

View File

@@ -0,0 +1,9 @@
import request from '@/utils/request'
// 查询首页业务指标
export function getDashboardSummary() {
return request({
url: '/app/dashboard/summary',
method: 'get'
})
}

View File

@@ -76,9 +76,9 @@
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="付费价" align="center" prop="priceFen">
<el-table-column label="付费价" align="center" prop="priceFen">
<template slot-scope="scope">
<span v-if="scope.row.isAd == 3">¥{{ (scope.row.priceFen / 100).toFixed(2) }}</span>
<span v-if="scope.row.isAd == 3">¥{{ formatPrice(scope.row.priceFen) }} </span>
<span v-else>-</span>
</template>
</el-table-column>
@@ -100,7 +100,7 @@
@pagination="getList" />
<!-- 添加或修改资源对话框 -->
<el-dialog :title="title" :visible.sync="open" width="50%" append-to-body>
<el-dialog :title="title" :visible.sync="open" width="72%" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="资源标题" prop="resourceTitle">
<el-input v-model="form.resourceTitle" placeholder="请输入资源标题" />
@@ -140,10 +140,6 @@
<el-form-item v-if="form.isAd == 2" label="兑换积分" prop="adNumber">
<el-input v-model="form.adNumber" placeholder="请输入需要兑换多少积分解锁" />
</el-form-item>
<el-form-item v-if="form.isAd == 3" label="价格(分)" prop="priceFen">
<el-input-number v-model="form.priceFen" :min="1" :step="100" controls-position="right" />
<span class="form-tip">系统将按价格自动匹配已配置的微信道具</span>
</el-form-item>
<el-form-item label="权重" prop="weight">
<el-input v-model="form.weight" placeholder="请输入权重" />
</el-form-item>
@@ -151,7 +147,15 @@
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
</el-form-item>
<el-divider content-position="center">资源列信息</el-divider>
<el-divider content-position="center">{{ form.isAd == 3 ? '资源规格' : '资源列信息' }}</el-divider>
<el-alert
v-if="form.isAd == 3"
title="排序代表版本等级:高排序包含低排序,价格必须递增;每个原价和可能产生的升级差价都要配置微信道具档位。"
type="info"
:closable="false"
show-icon
class="mb8"
/>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAddAppResourceList">添加</el-button>
@@ -165,19 +169,50 @@
@selection-change="handleAppResourceListSelectionChange" ref="appResourceList">
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="序号" align="center" prop="index" width="50" />
<el-table-column label="资源名称" prop="listName">
<el-table-column :label="form.isAd == 3 ? '规格名称' : '资源名称'" prop="listName" min-width="150">
<template slot-scope="scope">
<el-input v-model="scope.row.listName" placeholder="请输入资源名称" />
<el-input v-model="scope.row.listName" :placeholder="form.isAd == 3 ? '如:源码版' : '请输入资源名称'" />
</template>
</el-table-column>
<el-table-column label="资源地址" prop="listUrl">
<el-table-column v-if="form.isAd == 3" label="价格(分)" prop="priceFen" width="150">
<template slot-scope="scope">
<el-input v-model="scope.row.listUrl" placeholder="请输入资源地址" />
<el-input-number
v-model="scope.row.priceFen"
:min="1"
:step="100"
controls-position="right"
style="width: 130px"
/>
</template>
</el-table-column>
<el-table-column label="访问密码" prop="password">
<el-table-column label="资源地址" prop="listUrl" min-width="220">
<template slot-scope="scope">
<el-input v-model="scope.row.password" placeholder="请输入访问密码" />
<el-input
v-model="scope.row.listUrl"
:name="`resource-url-${scope.$index}`"
autocomplete="off"
placeholder="请输入资源地址"
/>
</template>
</el-table-column>
<el-table-column label="访问密码" prop="password" width="130">
<template slot-scope="scope">
<el-input
v-model="scope.row.password"
:name="`resource-access-code-${scope.$index}`"
autocomplete="off"
placeholder="请输入访问密码"
/>
</template>
</el-table-column>
<el-table-column v-if="form.isAd == 3" label="排序" prop="sortOrder" width="100">
<template slot-scope="scope">
<el-input-number v-model="scope.row.sortOrder" :min="0" controls-position="right" style="width: 80px" />
</template>
</el-table-column>
<el-table-column v-if="form.isAd == 3" label="状态" prop="status" width="90">
<template slot-scope="scope">
<el-switch v-model="scope.row.status" :active-value="1" :inactive-value="0" />
</template>
</el-table-column>
</el-table>
@@ -267,11 +302,6 @@
message: "需要广告不能为空",
trigger: "change"
}],
priceFen: [{
required: true,
message: "付费价格不能为空",
trigger: "blur"
}],
},
directory:"appimg/fengmian/"
};
@@ -353,7 +383,12 @@
const id = row.id || this.ids
getResource(id).then(response => {
this.form = response.data;
this.appResourceListList = response.data.appResourceListList;
this.appResourceListList = (response.data.appResourceListList || []).map((item, index) => ({
...item,
priceFen: item.priceFen || response.data.priceFen || null,
status: item.status === 0 ? 0 : 1,
sortOrder: item.sortOrder === null || item.sortOrder === undefined ? index : item.sortOrder
}));
this.open = true;
this.title = "修改资源";
});
@@ -362,6 +397,9 @@
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
if (!this.validateResourceSpecs()) {
return;
}
this.form.appResourceListList = this.appResourceListList;
if (this.form.id != null) {
updateResource(this.form).then(response => {
@@ -402,6 +440,9 @@
obj.listName = "";
obj.listUrl = "";
obj.password = "";
obj.priceFen = this.form.isAd == 3 ? (this.form.priceFen || null) : 0;
obj.status = 1;
obj.sortOrder = this.appResourceListList.length;
this.appResourceListList.push(obj);
},
/** 资源列删除按钮操作 */
@@ -420,6 +461,60 @@
handleAppResourceListSelectionChange(selection) {
this.checkedAppResourceList = selection.map(item => item.index)
},
validateResourceSpecs() {
const invalidPasswordItem = this.appResourceListList.find(item => {
return item.password && /^https?:\/\//i.test(item.password.trim())
})
if (invalidPasswordItem) {
this.$modal.msgError(`${invalidPasswordItem.listName || '未命名资源'}”的访问密码不能填写网盘链接`)
return false
}
if (this.form.isAd != 3) {
return true
}
if (!this.appResourceListList.length) {
this.$modal.msgError("付费资源至少需要配置一个规格")
return false
}
for (let index = 0; index < this.appResourceListList.length; index++) {
const item = this.appResourceListList[index]
if (!item.listName || !item.listName.trim()) {
this.$modal.msgError(`${index + 1}个规格名称不能为空`)
return false
}
if (!item.listUrl || !item.listUrl.trim()) {
this.$modal.msgError(`规格“${item.listName}”的资源地址不能为空`)
return false
}
if (!item.priceFen || item.priceFen <= 0) {
this.$modal.msgError(`规格“${item.listName}”的价格必须大于0分`)
return false
}
}
const orderedSpecs = [...this.appResourceListList].sort((left, right) => {
return Number(left.sortOrder || 0) - Number(right.sortOrder || 0)
})
for (let index = 1; index < orderedSpecs.length; index++) {
const previous = orderedSpecs[index - 1]
const current = orderedSpecs[index]
if (Number(current.sortOrder || 0) === Number(previous.sortOrder || 0)) {
this.$modal.msgError("版本排序不能重复,请按等级从低到高填写")
return false
}
if (Number(current.priceFen) <= Number(previous.priceFen)) {
this.$modal.msgError("版本价格必须随排序等级递增")
return false
}
}
if (!this.appResourceListList.some(item => item.status === 1)) {
this.$modal.msgError("至少需要启用一个购买规格")
return false
}
return true
},
formatPrice(priceFen) {
return (Number(priceFen || 0) / 100).toFixed(2)
},
/** 导出按钮操作 */
handleExport() {
this.download('app/resource/export', {

View File

@@ -94,6 +94,7 @@
<template slot-scope="scope">
<div>{{ scope.row.resourceTitle || '-' }}</div>
<div class="secondary-text">ID: {{ scope.row.resourceId }}</div>
<div class="secondary-text">规格{{ scope.row.specName || '旧版整项资源' }}</div>
</template>
</el-table-column>
<el-table-column label="金额" align="right" width="100">
@@ -144,6 +145,8 @@
<el-descriptions-item label="OpenID">{{ detail.openId || '-' }}</el-descriptions-item>
<el-descriptions-item label="资源ID">{{ detail.resourceId }}</el-descriptions-item>
<el-descriptions-item label="资源标题">{{ detail.resourceTitle || '-' }}</el-descriptions-item>
<el-descriptions-item label="规格ID">{{ detail.resourceListId || '旧版整项' }}</el-descriptions-item>
<el-descriptions-item label="购买规格">{{ detail.specName || '旧版整项资源' }}</el-descriptions-item>
<el-descriptions-item label="微信道具ID">{{ detail.productId || '-' }}</el-descriptions-item>
<el-descriptions-item label="微信内部订单号">{{ detail.wxOrderNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="微信交易号" :span="2">{{ detail.transactionId || '-' }}</el-descriptions-item>

View File

@@ -1,7 +1,38 @@
<template>
<div class="app-container home">
<el-row :gutter="20" class="dashboard-stats" v-loading="summaryLoading">
<el-col
v-for="item in metricCards"
:key="item.key"
:xs="24"
:sm="12"
:lg="6"
class="dashboard-stat-col"
>
<el-card shadow="hover" class="stat-card">
<div class="stat-card__content">
<div class="stat-card__icon" :class="`stat-card__icon--${item.theme}`">
<i :class="item.icon"></i>
</div>
<div class="stat-card__body">
<div class="stat-card__title">{{ item.title }}</div>
<div class="stat-card__value">
<span v-if="item.money">¥</span>{{ formatMetric(item.value, item.money) }}
</div>
<div class="stat-card__today">
<span>今日新增</span>
<span class="stat-card__today-value">
+<span v-if="item.money">¥</span>{{ formatMetric(item.today, item.money) }}
</span>
</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :sm="24" :lg="24">
<el-col :xs="24" :sm="24" :lg="12">
<!-- 每日新增用户折线图 -->
<el-card style="margin-bottom: 20px;">
<div slot="header" class="clearfix">
@@ -9,7 +40,9 @@
</div>
<div ref="userChart" style="height: 400px;"></div>
</el-card>
</el-col>
<el-col :xs="24" :sm="24" :lg="12">
<!-- 每日新增支付金额折线图 -->
<el-card style="margin-bottom: 20px;">
<div slot="header" class="clearfix">
@@ -958,6 +991,7 @@
<script>
import { getUserCountByDay } from '@/api/system/user';
import { getDailyPayAmount } from '@/api/app/payStatistics';
import { getDashboardSummary } from '@/api/app/dashboard';
import * as echarts from 'echarts';
export default {
@@ -975,14 +1009,85 @@ export default {
payStats: {
dates: [],
amounts: []
},
summaryLoading: false,
dashboardSummary: {
resourceTotal: 0,
resourceToday: 0,
userTotal: 0,
userToday: 0,
orderTotal: 0,
orderToday: 0,
amountTotal: 0,
amountToday: 0
}
};
},
computed: {
metricCards() {
return [
{
key: 'resource',
title: '资源总数',
value: this.dashboardSummary.resourceTotal,
today: this.dashboardSummary.resourceToday,
icon: 'el-icon-folder-opened',
theme: 'blue',
money: false
},
{
key: 'user',
title: '用户总数',
value: this.dashboardSummary.userTotal,
today: this.dashboardSummary.userToday,
icon: 'el-icon-user',
theme: 'green',
money: false
},
{
key: 'order',
title: '订单总数',
value: this.dashboardSummary.orderTotal,
today: this.dashboardSummary.orderToday,
icon: 'el-icon-shopping-cart-full',
theme: 'orange',
money: false
},
{
key: 'amount',
title: '订单总金额',
value: this.dashboardSummary.amountTotal,
today: this.dashboardSummary.amountToday,
icon: 'el-icon-money',
theme: 'purple',
money: true
}
];
}
},
mounted() {
this.loadDashboardSummary();
this.initUserChart();
this.initPayChart();
},
methods: {
loadDashboardSummary() {
this.summaryLoading = true;
getDashboardSummary().then(response => {
this.dashboardSummary = Object.assign({}, this.dashboardSummary, response.data || {});
}).finally(() => {
this.summaryLoading = false;
});
},
formatMetric(value, money) {
const number = Number(value || 0);
return number.toLocaleString('zh-CN', money ? {
minimumFractionDigits: 2,
maximumFractionDigits: 2
} : {
maximumFractionDigits: 0
});
},
goTarget(href) {
window.open(href, "_blank");
},
@@ -1082,6 +1187,109 @@ export default {
</script>
<style scoped lang="scss">
.dashboard-stats {
margin-bottom: 0;
}
.dashboard-stat-col {
margin-bottom: 20px;
}
.stat-card {
border: 0;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.06);
&__content {
display: flex;
align-items: center;
min-height: 116px;
}
&__icon {
display: flex;
flex: 0 0 64px;
align-items: center;
justify-content: center;
width: 64px;
height: 64px;
margin-right: 18px;
border-radius: 16px;
font-size: 30px;
&--blue {
color: #409eff;
background: rgba(64, 158, 255, 0.12);
}
&--green {
color: #36b37e;
background: rgba(54, 179, 126, 0.12);
}
&--orange {
color: #e6a23c;
background: rgba(230, 162, 60, 0.14);
}
&--purple {
color: #8b5cf6;
background: rgba(139, 92, 246, 0.12);
}
}
&__body {
min-width: 0;
flex: 1;
}
&__title {
color: #909399;
font-size: 14px;
}
&__value {
overflow: hidden;
margin-top: 8px;
color: #303133;
font-size: 27px;
font-weight: 600;
line-height: 34px;
text-overflow: ellipsis;
white-space: nowrap;
}
&__today {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 9px;
color: #909399;
font-size: 13px;
}
&__today-value {
color: #36b37e;
font-weight: 500;
}
}
@media (max-width: 1440px) {
.stat-card {
&__icon {
flex-basis: 54px;
width: 54px;
height: 54px;
margin-right: 12px;
font-size: 26px;
}
&__value {
font-size: 23px;
}
}
}
.home {
blockquote {
padding: 10px 20px;

View File

@@ -17,34 +17,14 @@
<!-- @keyup.enter.native="handleQuery"-->
<!-- />-->
<!-- </el-form-item>-->
<el-form-item label="项目技术" prop="codeTechnology">
<el-form-item label="其他技术" prop="codeTechnology">
<el-input
v-model="queryParams.codeTechnology"
placeholder="请输入项目技术"
placeholder="请输入其他技术"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="项目来源" prop="codeSource">
<el-select v-model="queryParams.codeSource" placeholder="请选择项目来源" clearable>
<el-option
v-for="dict in dict.type.code_source"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="付费方式" prop="paymentType">
<el-select v-model="queryParams.paymentType" placeholder="请选择付费方式" clearable>
<el-option
v-for="dict in dict.type.payment_type"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<!-- <el-form-item label="网盘地址" prop="diskLink">-->
<!-- <el-input-->
<!-- v-model="queryParams.diskLink"-->
@@ -124,17 +104,7 @@
<el-table-column label="项目名称" align="center" prop="codeName"/>
<!-- <el-table-column label="项目描述" align="center" prop="codeDesc" />-->
<el-table-column label="运行环境" align="center" prop="codeEnvironment"/>
<el-table-column label="项目技术" align="center" prop="codeTechnology"/>
<el-table-column label="项目来源" align="center" prop="codeSource">
<template slot-scope="scope">
<dict-tag :options="dict.type.code_source" :value="scope.row.codeSource"/>
</template>
</el-table-column>
<el-table-column label="付费方式" align="center" prop="paymentType">
<template slot-scope="scope">
<dict-tag :options="dict.type.payment_type" :value="scope.row.paymentType"/>
</template>
</el-table-column>
<el-table-column label="其他技术" align="center" prop="codeTechnology"/>
<!-- <el-table-column label="网盘地址" align="center" prop="diskLink" />-->
<el-table-column label="是否发布" align="center" prop="publishFlag">
<template slot-scope="scope">
@@ -218,31 +188,17 @@
<el-form-item label="运行环境" prop="codeEnvironment">
<el-input v-model="form.codeEnvironment" placeholder="请输入运行环境"/>
</el-form-item>
<el-form-item label="项目技术" prop="codeTechnology">
<el-input v-model="form.codeTechnology" placeholder="请输入项目技术"/>
<el-form-item label="前端" prop="frontendTechnology">
<el-input v-model="form.frontendTechnology" placeholder="请输入前端技术"/>
</el-form-item>
<el-form-item label="项目来源" prop="codeSource">
<el-select v-model="form.codeSource" placeholder="请选择项目来源">
<el-option
v-for="dict in dict.type.code_source"
:key="dict.value"
:label="dict.label"
:value="dict.value"
></el-option>
</el-select>
<el-form-item label="后端" prop="backendTechnology">
<el-input v-model="form.backendTechnology" placeholder="请输入后端技术"/>
</el-form-item>
<el-form-item label="付费方式" prop="paymentType">
<el-radio-group v-model="form.paymentType">
<el-radio
v-for="dict in dict.type.payment_type"
:key="dict.value"
:label="dict.value"
>{{dict.label}}
</el-radio>
</el-radio-group>
<el-form-item label="数据库" prop="databaseTechnology">
<el-input v-model="form.databaseTechnology" placeholder="请输入数据库技术"/>
</el-form-item>
<el-form-item label="网盘地址" prop="diskLink">
<el-input v-model="form.diskLink" placeholder="请输入网盘地址"/>
<el-form-item label="其他技术" prop="codeTechnology">
<el-input v-model="form.codeTechnology" placeholder="请输入其他技术"/>
</el-form-item>
<el-form-item label="是否发布" prop="publishFlag">
<el-radio-group v-model="form.publishFlag">
@@ -254,12 +210,6 @@
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="截图文件" prop="pictureFile">
<file-upload v-model="form.pictureFile"/>
</el-form-item>
<el-form-item label="视频文件" prop="videoFile">
<file-upload v-model="form.videoFile"/>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
@@ -275,27 +225,19 @@
<span v-html="form.codeDesc"/>
</el-descriptions-item>
<el-descriptions-item label="运行环境" :span="4">{{ form.codeEnvironment }}</el-descriptions-item>
<el-descriptions-item label="项目技术" :span="4">{{ form.codeTechnology }}</el-descriptions-item>
<el-descriptions-item label="前端" :span="4">{{ form.frontendTechnology }}</el-descriptions-item>
<el-descriptions-item label="后端" :span="4">{{ form.backendTechnology }}</el-descriptions-item>
<el-descriptions-item label="数据库" :span="4">{{ form.databaseTechnology }}</el-descriptions-item>
<el-descriptions-item label="其他技术" :span="4">{{ form.codeTechnology }}</el-descriptions-item>
<!-- <el-descriptions-item label="项目来源">-->
<!-- <dict-tag :options="dict.type.code_source" :value="form.codeSource"/>-->
<!-- </el-descriptions-item>-->
<!-- <el-descriptions-item label="付费方式">-->
<!-- <dict-tag :options="dict.type.payment_type" :value="form.paymentType"/>-->
<!-- </el-descriptions-item>-->
<el-descriptions-item label="网盘地址" :span="2">{{ form.diskLink }}</el-descriptions-item>
<el-descriptions-item label="是否发布" :span="2">
<el-descriptions-item label="是否发布" :span="4">
<dict-tag :options="dict.type.sys_yes_no" :value="form.publishFlag"/>
</el-descriptions-item>
<el-descriptions-item label="截图文件" :span="4">
<el-link v-if="form.pictureFile" :href="`${baseUrl}${form.pictureFile}`" :underline="false" target="_blank">
<span class="el-icon-document"> 下载 </span>
</el-link>
</el-descriptions-item>
<el-descriptions-item label="视频文件" :span="4">
<el-link v-if="form.videoFile" :href="`${baseUrl}${form.videoFile}`" :underline="false" target="_blank">
<span class="el-icon-document"> 下载 </span>
</el-link>
</el-descriptions-item>
<el-descriptions-item label="截图展示" :span="4">
<el-image
style="width: 100px; height: 80px; padding: 3px;"
@@ -373,6 +315,9 @@
codeDesc: null,
codeEnvironment: null,
codeTechnology: null,
frontendTechnology: null,
backendTechnology: null,
databaseTechnology: null,
codeSource: null,
paymentType: null,
diskLink: null,
@@ -430,6 +375,9 @@
codeDesc: null,
codeEnvironment: 'jdk1.8 + mysql5.7以上 + idea + vscode',
codeTechnology: 'springboot + vue2 + elementui + nodejs14',
frontendTechnology: null,
backendTechnology: null,
databaseTechnology: null,
codeSource: null,
paymentType: null,
diskLink: null,
@@ -484,6 +432,9 @@
content = content.replace(/{projectName}/g, projectName);
content = content.replace(/{codeDesc}/g, plainDesc);
content = content.replace(/{codeEnvironment}/g, this.form.codeEnvironment || '');
content = content.replace(/{frontendTechnology}/g, this.form.frontendTechnology || '');
content = content.replace(/{backendTechnology}/g, this.form.backendTechnology || '');
content = content.replace(/{databaseTechnology}/g, this.form.databaseTechnology || '');
content = content.replace(/{codeTechnology}/g, this.form.codeTechnology || '');
content = content.replace(/{diskLink}/g, this.form.diskLink || '');
content = content.replace(/{screenshots}/g, screenshots);

View File

@@ -89,7 +89,10 @@
{projectName} 纯项目名(去掉编号和基于...实现的前缀)
{codeDesc} 项目描述(纯文本)
{codeEnvironment} 运行环境
{codeTechnology} 项目技术
{frontendTechnology} 前端
{backendTechnology} 后端
{databaseTechnology} 数据库
{codeTechnology} 其他技术
{diskLink} 网盘地址
{screenshots} 截图列表(自动生成 markdown 格式)"
/>
@@ -102,7 +105,10 @@
<code>{projectName}</code> 纯项目名 &nbsp;
<code>{codeDesc}</code> 描述(纯文本) &nbsp;
<code>{codeEnvironment}</code> 运行环境 &nbsp;
<code>{codeTechnology}</code> 项目技术 &nbsp;
<code>{frontendTechnology}</code> 前端 &nbsp;
<code>{backendTechnology}</code> 后端 &nbsp;
<code>{databaseTechnology}</code> 数据库 &nbsp;
<code>{codeTechnology}</code> 其他技术 &nbsp;
<code>{diskLink}</code> 网盘地址 &nbsp;
<code>{screenshots}</code> 截图列表
</template>
@@ -229,4 +235,4 @@ export default {
}
}
};
</script>
</script>

View File

@@ -82,6 +82,21 @@
v-hasPermi="['office:project:export']"
>导出</el-button>
</el-col>
<el-col :span="1.5">
<el-dropdown
trigger="click"
@command="handleLinkImport"
v-hasPermi="['office:project:edit']"
>
<el-button type="primary" plain icon="el-icon-upload2" size="mini">
导入链接<i class="el-icon-arrow-down el-icon--right"></i>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="QUARK">导入夸克链接</el-dropdown-item>
<el-dropdown-item command="BAIDU">导入百度链接</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-col>
<el-col :span="1.5">
<el-button
type="info"
@@ -97,10 +112,22 @@
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="projectList" @cell-dblclick="copyText" @selection-change="handleSelectionChange">
<el-table
v-loading="loading"
:data="projectList"
@cell-dblclick="copyText"
@selection-change="handleSelectionChange"
@sort-change="handleSortChange"
>
<el-table-column type="selection" width="55" align="center" />
<!-- <el-table-column label="${comment}" align="center" prop="id" />-->
<el-table-column label="项目编号" align="center" prop="projectNum" />
<el-table-column
label="项目编号"
align="center"
prop="projectNum"
sortable="custom"
:sort-orders="['ascending', 'descending']"
/>
<el-table-column label="项目名称" align="center" prop="projectName" />
<el-table-column label="源码名称" align="center" prop="projectName1" />
<el-table-column label="项目技术" align="center" prop="projectNum1" />
@@ -241,6 +268,94 @@
</div>
</el-dialog>
<!-- 网盘链接导入对话框 -->
<el-dialog
:title="linkUpload.title"
:visible.sync="linkUpload.open"
width="460px"
append-to-body
@closed="resetLinkUpload"
>
<el-upload
ref="linkUpload"
:limit="1"
accept=".csv,.xls,.xlsx"
:headers="linkUpload.headers"
:action="linkUpload.url + '?diskType=' + linkUpload.diskType"
:disabled="linkUpload.isUploading"
:before-upload="beforeLinkFileUpload"
:on-progress="handleLinkFileUploadProgress"
:on-success="handleLinkFileSuccess"
:on-error="handleLinkFileError"
:auto-upload="false"
drag
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">
支持网盘客户端导出的 csvxlsxlsx 文件单个文件不超过 5MB
<div class="import-rule-tip">
按完整项目名称精确匹配不存在时自动新增存在时仅更新对应网盘链接
</div>
</div>
</el-upload>
<div slot="footer" class="dialog-footer">
<el-button
type="primary"
:loading="linkUpload.isUploading"
@click="submitLinkFileForm"
> </el-button>
<el-button @click="linkUpload.open = false"> </el-button>
</div>
</el-dialog>
<!-- 网盘链接导入结果 -->
<el-dialog
title="导入结果"
:visible.sync="importResult.open"
width="760px"
append-to-body
>
<div v-if="importResult.data" class="import-result">
<el-alert
:title="importResult.message"
:type="importResult.data.failedCount || importResult.data.skippedCount ? 'warning' : 'success'"
:closable="false"
show-icon
/>
<div class="import-result-summary">
<span>总计{{ importResult.data.totalCount || 0 }}</span>
<span class="result-added">新增{{ importResult.data.addedCount || 0 }}</span>
<span class="result-updated">更新{{ importResult.data.updatedCount || 0 }}</span>
<span>未变化{{ importResult.data.unchangedCount || 0 }}</span>
<span class="result-skipped">跳过{{ importResult.data.skippedCount || 0 }}</span>
<span class="result-failed">失败{{ importResult.data.failedCount || 0 }}</span>
</div>
<el-table
v-if="importResult.data.details && importResult.data.details.length"
:data="importResult.data.details"
max-height="360"
border
size="small"
>
<el-table-column label="行号" prop="rowNumber" width="70" align="center" />
<el-table-column label="项目名称" prop="projectName" min-width="210" show-overflow-tooltip />
<el-table-column label="结果" width="90" align="center">
<template slot-scope="scope">
<el-tag
size="mini"
:type="scope.row.resultType === 'FAILED' ? 'danger' : 'warning'"
>{{ scope.row.resultType === 'FAILED' ? '失败' : '跳过' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="原因" prop="reason" min-width="260" show-overflow-tooltip />
</el-table>
</div>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="importResult.open = false"> </el-button>
</div>
</el-dialog>
<!-- 封面预览对话框 -->
<el-dialog title="封面预览" :visible.sync="previewVisible" width="40%" append-to-body>
<div class="preview-container" style="text-align: center;">
@@ -297,6 +412,8 @@ export default {
queryParams: {
pageNum: 1,
pageSize: 10,
orderByColumn: null,
isAsc: null,
projectNum: null,
projectNum1: null,
projectName: null,
@@ -310,6 +427,21 @@ export default {
form: {},
// 表单校验
rules: {},
// 网盘链接导入参数
linkUpload: {
open: false,
title: '',
diskType: '',
isUploading: false,
headers: { Authorization: 'Bearer ' + getToken() },
url: process.env.VUE_APP_BASE_API + '/office/project/import-links'
},
// 网盘链接导入结果
importResult: {
open: false,
message: '',
data: null
},
previewVisible: false,
previewUrl: '',
previewBlob: null,
@@ -365,6 +497,13 @@ export default {
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 项目编号排序 */
handleSortChange({ prop, order }) {
this.queryParams.orderByColumn = prop
this.queryParams.isAsc = order
this.queryParams.pageNum = 1
this.getList()
},
/** 检测单个项目的网盘链接 */
handleCheckRow(row) {
if (this.isChecking(row.id)) {
@@ -516,6 +655,73 @@ export default {
...this.queryParams
}, `project_${new Date().getTime()}.xlsx`)
},
/** 打开网盘链接导入对话框 */
handleLinkImport(diskType) {
this.linkUpload.diskType = diskType
this.linkUpload.title = diskType === 'QUARK' ? '导入夸克网盘链接' : '导入百度网盘链接'
this.linkUpload.open = true
this.$nextTick(() => {
if (this.$refs.linkUpload) {
this.$refs.linkUpload.clearFiles()
}
})
},
/** 导入文件校验 */
beforeLinkFileUpload(file) {
const extension = file.name.substring(file.name.lastIndexOf('.')).toLowerCase()
const isSupported = ['.csv', '.xls', '.xlsx'].indexOf(extension) !== -1
const isWithinLimit = file.size / 1024 / 1024 <= 5
if (!isSupported) {
this.$modal.msgError('仅支持 csv、xls、xlsx 格式文件')
return false
}
if (!isWithinLimit) {
this.$modal.msgError('导入文件不能超过 5MB')
return false
}
return true
},
/** 网盘链接文件上传中 */
handleLinkFileUploadProgress() {
this.linkUpload.isUploading = true
},
/** 网盘链接文件上传成功 */
handleLinkFileSuccess(response) {
this.linkUpload.isUploading = false
if (!response || response.code !== 200) {
this.$modal.msgError(response && response.msg ? response.msg : '导入失败')
return
}
this.linkUpload.open = false
this.importResult.message = response.msg || '导入完成'
this.importResult.data = response.data || {}
this.importResult.open = true
this.getList()
},
/** 网盘链接文件上传失败 */
handleLinkFileError(error) {
this.linkUpload.isUploading = false
let message = '导入失败'
if (error && error.message) {
message = error.message
}
this.$modal.msgError(message)
},
/** 提交网盘链接导入文件 */
submitLinkFileForm() {
if (!this.$refs.linkUpload || !this.$refs.linkUpload.uploadFiles.length) {
this.$modal.msgWarning('请选择需要导入的文件')
return
}
this.$refs.linkUpload.submit()
},
/** 重置网盘链接上传状态 */
resetLinkUpload() {
this.linkUpload.isUploading = false
if (this.$refs.linkUpload) {
this.$refs.linkUpload.clearFiles()
}
},
copyText(row, column, cell, event) {
if (column.label !== '操作') {
// 双击复制
@@ -534,14 +740,14 @@ export default {
let content = ''
if (row.projectUrl) {
content += '夸克网盘\n'
content += '链接:'+ row.projectUrl
content += '链接:' + row.projectUrl
}
if (content.length > 0) {
content += '\n\n'
}
if (row.projectBaiduUrl) {
content += '百度网盘\n'
content += '链接:'+ row.projectBaiduUrl
content += '链接:' + row.projectBaiduUrl
}
navigator.clipboard.writeText(content).then(() => {
this.$message({
@@ -559,14 +765,14 @@ export default {
let content = ''
if (row.projectUrl) {
content += '夸克网盘\n'
content += '链接:'+ row.projectUrl
content += '链接:' + row.projectUrl
}
if (content.length > 0) {
content += '\n\n'
}
if (row.projectBaiduUrl) {
content += '百度网盘\n'
content += '链接:'+ row.projectBaiduUrl
content += '链接:' + row.projectBaiduUrl
}
content += '\n\n项目部署文档\nhttps://www.yuque.com/feastcoding/cg5w9h/rbvp8w80h8tultgy 密码el4q'
navigator.clipboard.writeText(content).then(() => {
@@ -655,4 +861,31 @@ export default {
color: #606266;
text-align: right;
}
.import-rule-tip {
margin-top: 6px;
color: #909399;
line-height: 20px;
}
.import-result-summary {
display: flex;
flex-wrap: wrap;
gap: 18px;
margin: 18px 0;
color: #606266;
}
.result-added,
.result-updated {
color: #67c23a;
}
.result-skipped {
color: #e6a23c;
}
.result-failed {
color: #f56c6c;
}
</style>