feat: 用户套餐,用户支付功能

This commit is contained in:
xiaojunnuo
2024-12-22 14:00:46 +08:00
parent d70e2b66a3
commit a019956698
69 changed files with 2071 additions and 738 deletions
@@ -0,0 +1,33 @@
<template>
<span class="cd-expires-time-text">
<template v-if="label != null">
{{ label }}
</template>
<template v-else>
<FsTimeHumanize :model-value="value" :use-format-greater="1000000000000" :options="{ units: ['d'] }"></FsTimeHumanize>
</template>
</span>
</template>
<script lang="ts" setup>
import dayjs from "dayjs";
import { computed } from "vue";
defineOptions({
name: "ExpiresTimeText"
});
const props = defineProps<{
value?: number;
}>();
const label = computed(() => {
if (props.value == null) {
return "";
}
if (props.value === -1) {
return "永久";
}
return null;
});
</script>
@@ -9,6 +9,7 @@ import "@vue-js-cron/light/dist/light.css";
import Plugins from "./plugins/index";
import LoadingButton from "./loading-button.vue";
import IconSelect from "./icon-select.vue";
import ExpiresTimeText from "./expires-time-text.vue";
export default {
install(app: any) {
app.component("PiContainer", PiContainer);
@@ -25,7 +26,7 @@ export default {
app.component("LoadingButton", LoadingButton);
app.component("IconSelect", IconSelect);
app.component("ExpiresTimeText", ExpiresTimeText);
app.use(vip);
app.use(Plugins);
}
@@ -231,13 +231,13 @@ function openUpgrade() {
title: "基础版",
desc: "免费使用",
type: "free",
privilege: ["证书申请功能无限制", "证书流水线数量10条", "常用的主机、cdn等部署插件"]
privilege: ["证书申请功能无限制", "证书流水线数量无限制", "常用的主机、云平台、cdn等部署插件"]
},
plus: {
title: "专业版",
desc: "功能增强,适用于个人企业内部使用",
type: "plus",
privilege: ["可加VIP群,需求优先实现", "证书流水线数量无限制", "免配置发邮件功能", "支持宝塔、易盾、群晖、1Panel、cdnfly等部署插件"],
privilege: ["可加VIP群,需求优先实现", "宝塔、群晖、1Panel、易盾等部署插件", "站点证书监控", "更多通知种类"],
trial: {
title: "7天试用",
click: () => {
@@ -71,7 +71,7 @@ export const certdResources = [
}
},
{
title: "分组管理",
title: "流水线分组管理",
name: "PipelineGroupManager",
path: "/certd/pipeline/group",
component: "/certd/pipeline/group/index.vue",
@@ -90,6 +90,47 @@ export const certdResources = [
auth: true
}
},
{
title: "我的订单",
name: "MyTrade",
path: "/certd/trade",
component: "/certd/trade/index.vue",
meta: {
icon: "ion:person-outline",
auth: true
}
},
{
title: "支付返回",
name: "PaymentReturn",
path: "/certd/payment/return/:type",
component: "/certd/payment/return.vue",
meta: {
icon: "ion:person-outline",
auth: false,
isMenu: false
}
},
{
title: "站点证书监控",
name: "SiteCertMonitor",
path: "/certd/monitor/site",
component: "/certd/monitor/site/index.vue",
meta: {
icon: "ion:person-outline",
auth: true
}
},
{
title: "证书仓库",
name: "CertStore",
path: "/certd/monitor/cert",
component: "/certd/monitor/cert/index.vue",
meta: {
icon: "ion:person-outline",
auth: true
}
},
// {
// title: "邮箱设置",
// name: "EmailSetting",
@@ -171,46 +171,24 @@ export const sysResources = [
}
},
{
title: "套餐支付",
name: "SuiteManager",
path: "/sys/suite",
redirect: "/sys/suite/product",
title: "套餐设置",
name: "SuiteSetting",
path: "/sys/suite/setting",
component: "/sys/suite/setting/index.vue",
meta: {
icon: "ion:golf-outline",
permission: "sys:settings:view"
},
children: [
{
title: "套餐管理",
name: "ProductManager",
path: "/sys/suite/product",
component: "/sys/suite/product/index.vue",
meta: {
icon: "ion:person-outline",
permission: "sys:settings:edit"
}
},
{
title: "支付管理",
name: "PaymentManager",
path: "/sys/suite/payment",
component: "/sys/suite/payment/index.vue",
meta: {
icon: "ion:person-outline",
permission: "sys:settings:edit"
}
},
{
title: "订单管理",
name: "OrderManager",
path: "/sys/suite/order",
component: "/sys/suite/order/index.vue",
meta: {
icon: "ion:person-outline",
permission: "sys:settings:edit"
}
}
]
icon: "ion:person-outline",
permission: "sys:settings:edit"
}
},
{
title: "订单管理",
name: "OrderManager",
path: "/sys/suite/trade",
component: "/sys/suite/trade/index.vue",
meta: {
icon: "ion:person-outline",
permission: "sys:settings:edit"
}
}
// {
@@ -1,7 +1,7 @@
import { request } from "/src/api/service";
export function createPaymentApi() {
const apiPrefix = "/sys/suite/payment";
export function createApi() {
const apiPrefix = "/pi/pipeline/group";
return {
async GetList(query: any) {
return await request({
@@ -42,35 +42,13 @@ export function createPaymentApi() {
params: { id }
});
},
async GetOptions(id: number) {
async ListAll() {
return await request({
url: apiPrefix + "/options",
url: apiPrefix + "/all",
method: "post"
});
},
async GetSimpleInfo(id: number) {
return await request({
url: apiPrefix + "/simpleInfo",
method: "post",
params: { id }
});
},
async GetDefineTypes() {
return await request({
url: apiPrefix + "/getTypeDict",
method: "post"
});
},
async GetProviderDefine(type: string) {
return await request({
url: apiPrefix + "/define",
method: "post",
params: { type }
});
}
};
}
export const pipelineGroupApi = createApi();
@@ -0,0 +1,178 @@
// @ts-ignore
import { useI18n } from "vue-i18n";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { pipelineGroupApi } from "./api";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();
const api = pipelineGroupApi;
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const editRequest = async (req: EditReq) => {
const { form, row } = req;
form.id = row.id;
const res = await api.UpdateObj(form);
return res;
};
const delRequest = async (req: DelReq) => {
const { row } = req;
return await api.DelObj(row.id);
};
const addRequest = async (req: AddReq) => {
const { form } = req;
const res = await api.AddObj(form);
return res;
};
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest
},
form: {
labelCol: {
//固定label宽度
span: null,
style: {
width: "100px"
}
},
col: {
span: 22
},
wrapper: {
width: 600
}
},
rowHandle: {
width: 200
},
columns: {
id: {
title: "ID",
key: "id",
type: "number",
search: {
show: false
},
column: {
width: 100,
editable: {
disabled: true
}
},
form: {
show: false
}
},
domain: {
title: "主域名",
search: {
show: true
},
type: "text",
form: {
show: false
},
column: {
width: 100,
sorter: true
}
},
domains: {
title: "全部域名",
search: {
show: false
},
type: "text",
form: {
rules: [{ required: true, message: "请输入域名" }]
},
column: {
width: 300,
sorter: true
}
},
domainCount: {
title: "域名数量",
type: "number",
form: {
show: false
},
column: {
width: 120,
sorter: true
}
},
pipelineId: {
title: "已关联流水线",
search: { show: false },
type: "text",
form: {
show: false
},
column: {
width: 200,
sorter: true
}
},
applyTime: {
title: "申请时间",
search: {
show: false
},
type: "datetime",
form: {
show: false
},
column: {
sorter: true
}
},
expiresTime: {
title: "过期时间",
search: {
show: true
},
type: "date",
form: {
show: false
},
column: {
sorter: true
}
},
fromType: {
title: "来源",
search: {
show: true
},
type: "text",
form: { show: false },
column: {
width: 100,
sorter: true
}
},
certProvider: {
title: "证书颁发机构",
search: {
show: true
},
type: "text",
form: {
show: false
},
column: {
width: 400
}
}
}
}
};
}
@@ -0,0 +1,30 @@
<template>
<fs-page>
<template #header>
<div class="title">
证书仓库
<span class="sub">管理证书支持手动上传证书并部署</span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</fs-page>
</template>
<script lang="ts" setup>
import { defineComponent, onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { createApi } from "./api";
defineOptions({
name: "CertStore"
});
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: {} });
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
</script>
@@ -0,0 +1,54 @@
import { request } from "/src/api/service";
export function createApi() {
const apiPrefix = "/pi/pipeline/group";
return {
async GetList(query: any) {
return await request({
url: apiPrefix + "/page",
method: "post",
data: query
});
},
async AddObj(obj: any) {
return await request({
url: apiPrefix + "/add",
method: "post",
data: obj
});
},
async UpdateObj(obj: any) {
return await request({
url: apiPrefix + "/update",
method: "post",
data: obj
});
},
async DelObj(id: number) {
return await request({
url: apiPrefix + "/delete",
method: "post",
params: { id }
});
},
async GetObj(id: number) {
return await request({
url: apiPrefix + "/info",
method: "post",
params: { id }
});
},
async ListAll() {
return await request({
url: apiPrefix + "/all",
method: "post"
});
}
};
}
export const pipelineGroupApi = createApi();
@@ -0,0 +1,228 @@
// @ts-ignore
import { useI18n } from "vue-i18n";
import { ref } from "vue";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { pipelineGroupApi } from "./api";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();
const api = pipelineGroupApi;
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const editRequest = async (req: EditReq) => {
const { form, row } = req;
form.id = row.id;
const res = await api.UpdateObj(form);
return res;
};
const delRequest = async (req: DelReq) => {
const { row } = req;
return await api.DelObj(row.id);
};
const addRequest = async (req: AddReq) => {
const { form } = req;
const res = await api.AddObj(form);
return res;
};
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest
},
form: {
labelCol: {
//固定label宽度
span: null,
style: {
width: "100px"
}
},
col: {
span: 22
},
wrapper: {
width: 600
}
},
rowHandle: {
width: 200
},
columns: {
id: {
title: "ID",
key: "id",
type: "number",
search: {
show: false
},
column: {
width: 100,
editable: {
disabled: true
}
},
form: {
show: false
}
},
name: {
title: "站点名称",
search: {
show: true
},
type: "text",
form: {
rules: [{ required: true, message: "请输入站点名称" }]
},
column: {
width: 100
}
},
domain: {
title: "主域名",
search: {
show: true
},
type: "text",
form: {
rules: [{ required: true, message: "请输入域名" }]
},
column: {
width: 100,
sorter: true
}
},
domains: {
title: "其他域名",
search: {
show: true
},
type: "text",
form: {
rules: [{ required: true, message: "请输入其他域名" }]
},
column: {
width: 300,
sorter: true
}
},
certInfo: {
title: "证书详情",
search: {
show: false
},
type: "text",
form: {
show: false
},
column: {
width: 100
}
},
certStatus: {
title: "证书状态",
search: {
show: true
},
type: "text",
form: {
show: false
},
column: {
width: 100,
sorter: true
}
},
certExpiresTime: {
title: "证书到期时间",
search: {
show: false
},
type: "date",
form: {
show: false
},
column: {
sorter: true
}
},
lastCheckTime: {
title: "上次检查时间",
search: {
show: false
},
type: "datetime",
form: {
show: false
},
column: {
sorter: true
}
},
checkStatus: {
title: "检查状态",
search: {
show: false
},
type: "text",
form: {
show: false
},
column: {
width: 100,
sorter: true
}
},
pipelineId: {
title: "关联流水线id",
search: {
show: false
},
type: "number",
column: {
width: 200,
sorter: true
}
},
certInfoId: {
title: "证书id",
search: {
show: false
},
type: "number",
form: {},
column: {
width: 100,
sorter: true,
show: false
}
},
disabled: {
title: "禁用启用",
search: {
show: false
},
type: "dict-switch",
dict: dict({
data: [
{ label: "禁用", value: true, color: "red" },
{ label: "启用", value: false, color: "green" }
]
}),
form: {},
column: {
width: 100,
sorter: true
}
}
}
}
};
}
@@ -0,0 +1,30 @@
<template>
<fs-page>
<template #header>
<div class="title">
站点证书监控
<span class="sub">监控网站证书的过期时间并发出提醒</span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</fs-page>
</template>
<script lang="ts" setup>
import { defineComponent, onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { createApi } from "./api";
defineOptions({
name: "SiteCertMonitor"
});
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: {} });
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
</script>
@@ -0,0 +1,11 @@
import { request } from "/src/api/service";
const apiPrefix = "/pay";
export async function Notify(type: string, query: any) {
return await request({
url: apiPrefix + `/notify/${type}`,
method: "post",
data: query,
returnResponse: true
});
}
@@ -0,0 +1,50 @@
<template>
<div class="cd-payment-return">
<a-card title="支付结果" class="mt-10">
<div class="flex-o">
<div class="flex-1">
<a-tag v-if="payResult" color="green" class="m-0">支付成功</a-tag>
<a-tag v-else color="red" class="m-0">支付失败</a-tag>
</div>
<div class="m-10">
<a-button type="primary" @click="goHome">回首页</a-button>
</div>
</div>
</a-card>
</div>
</template>
<script setup lang="ts">
import { Ref, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import * as api from "./api";
const route = useRoute();
const type = route.params.type as string;
const query = route.query;
async function checkNotify() {
const res = await api.Notify(type, query);
if (res === "success") {
return true;
}
return false;
}
const payResult: Ref = ref(null);
async function check() {
const pass = await checkNotify();
if (!pass) {
payResult.value = false;
} else {
payResult.value = true;
}
}
const router = useRouter();
function goHome() {
router.push({
path: "/"
});
}
</script>
@@ -2,7 +2,7 @@
<fs-page>
<template #header>
<div class="title">
分组管理
流水线分组管理
<span class="sub">管理流水线分组</span>
</div>
</template>
@@ -51,3 +51,17 @@ export async function TradeCreate(form: TradeCreateReq) {
data: form
});
}
export async function GetPaymentTypes() {
return await request({
url: "/suite/trade/payments",
method: "POST"
});
}
export async function GetSuiteSetting() {
return await request({
url: "/suite/settings/get",
method: "POST"
});
}
@@ -4,8 +4,14 @@
<div class="title">套餐购买</div>
</template>
<div class="suite-buy-content">
<a-row :gutter="12">
<a-col v-for="item of products" :key="item.id" class="mb-10" :xs="12" :sm="12" :md="8" :lg="6" :xl="6" :xxl="4">
<div class="mb-10">
<a-card>
<div>套餐说明多个套餐内的数量可以叠加</div>
<div v-if="suiteIntro" v-html="suiteIntro"></div>
</a-card>
</div>
<a-row :gutter="8">
<a-col v-for="item of products" :key="item.id" class="mb-10 suite-card-col">
<product-info :product="item" @order="doOrder" />
</a-col>
</a-row>
@@ -34,6 +40,13 @@ async function doOrder(req: any) {
...req
});
}
const suiteIntro = ref("");
async function loadSuiteIntro() {
const res = await api.GetSuiteSetting();
suiteIntro.value = res.intro;
}
loadSuiteIntro();
</script>
<style lang="less">
@@ -76,6 +89,11 @@ async function doOrder(req: any) {
margin-right: 10px;
}
}
.suite-card-col {
width: 20% !important;
min-width: 360px !important;
}
}
}
</style>
@@ -1,3 +0,0 @@
<div>
</div>
@@ -12,17 +12,13 @@
<div class="flex-o mt-5">
时长
<a-tag color="green"> {{ durationDict.dataMap[formRef.duration]?.label }}</a-tag>
<duration-value v-model="formRef.duration"></duration-value>
</div>
<div class="flex-o mt-5">价格 <price-input :edit="false" :model-value="durationSelected.price"></price-input></div>
<div class="flex-o mt-5">
支付方式
<a-select v-model:value="formRef.payType">
<a-select-option value="yizhifu">易支付</a-select-option>
<a-select-option value="alipay">支付宝</a-select-option>
<a-select-option value="wxpay">微信支付</a-select-option>
</a-select>
<fs-dict-select v-model:value="formRef.payType" :dict="paymentsDictRef" style="width: 200px"> </fs-dict-select>
</div>
</div>
</a-modal>
@@ -30,11 +26,13 @@
<script setup lang="ts">
import { ref } from "vue";
import { durationDict, OrderModalOpenReq, TradeCreate } from "/@/views/certd/suite/api";
import { GetPaymentTypes, OrderModalOpenReq, TradeCreate } from "/@/views/certd/suite/api";
import SuiteValue from "/@/views/sys/suite/product/suite-value.vue";
import PriceInput from "/@/views/sys/suite/product/price-input.vue";
import { notification } from "ant-design-vue";
import modal from "/@/views/certd/notification/notification-selector/modal/index.vue";
import { dict } from "@fast-crud/fast-crud";
import { notification } from "ant-design-vue";
import DurationValue from "/@/views/sys/suite/product/duration-value.vue";
const openRef = ref(false);
@@ -50,11 +48,26 @@ async function open(opts: OrderModalOpenReq) {
formRef.value.productId = opts.product.id;
formRef.value.duration = opts.duration;
formRef.value.num = opts.num ?? 1;
formRef.value.payType = "alipay";
}
const paymentsDictRef = dict({
async getData() {
return await GetPaymentTypes();
},
onReady: ({ dict }) => {
if (dict.data.length > 0) {
formRef.value.payType = dict.data[0].value;
}
}
});
async function orderCreate() {
console.log("orderCreate", formRef.value);
if (!formRef.value.payType) {
notification.error({
message: "请选择支付方式"
});
return;
}
const paymentReq = await TradeCreate({
productId: formRef.value.productId,
duration: formRef.value.duration,
@@ -1,11 +1,12 @@
<template>
<a-card :title="product.title" class="product-card">
<template #extra>
<a-tag>{{ product.type }}</a-tag>
<fs-values-format v-model="product.type" :dict="productTypeDictRef"></fs-values-format>
</template>
<div>{{ product.intro }}</div>
<div class="hr">
<div class="product-intro">{{ product.intro || "暂无介绍" }}</div>
<a-divider />
<div>
<div class="flex-between mt-5">
<div class="flex-o"><fs-icon icon="ant-design:check-outlined" class="color-green mr-5" /> 流水线条数</div>
<suite-value :model-value="product.content.maxPipelineCount" unit="条" />
@@ -20,13 +21,12 @@
</div>
<div class="flex-between mt-5">
<div class="flex-o"><fs-icon icon="ant-design:check-outlined" class="color-green mr-5" /> 证书监控</div>
<a-tag v-if="product.content.sproductonitor" color="green" class="m-0">支持</a-tag>
<a-tag v-else color="gray" class="m-0">不支持</a-tag>
<suite-value :model-value="product.content.maxMonitorCount" unit="个" />
</div>
</div>
<div class="duration flex-between mt-5 hr">
<div class="flex-o">时长</div>
<a-divider />
<div class="duration flex-between mt-5">
<div class="flex-o duration-label">时长</div>
<div class="duration-list">
<div
v-for="dp of product.durationPrices"
@@ -39,8 +39,8 @@
</div>
</div>
</div>
<div class="price flex-between mt-5 hr">
<a-divider />
<div class="price flex-between mt-5">
<div class="flex-o">价格</div>
<div class="flex-o price-text">
<price-input style="font-size: 18px; color: red" :model-value="selected?.price" :edit="false" />
@@ -58,12 +58,20 @@ import { durationDict } from "/@/views/certd/suite/api";
import SuiteValue from "/@/views/sys/suite/product/suite-value.vue";
import PriceInput from "/@/views/sys/suite/product/price-input.vue";
import { ref } from "vue";
import { dict } from "@fast-crud/fast-crud";
const props = defineProps<{
product: any;
}>();
const selected = ref(props.product.durationPrices[0]);
const productTypeDictRef = dict({
data: [
{ value: "suite", label: "套餐", color: "green" },
{ value: "addon", label: "加量包", color: "blue" }
]
});
const emit = defineEmits(["order"]);
async function doOrder() {
emit("order", { product: props.product, productId: props.product.id, duration: selected.value.duration });
@@ -72,8 +80,30 @@ async function doOrder() {
<style lang="less">
.product-card {
.ant-card-body {
padding: 20px;
padding-top: 10px;
padding-bottom: 10px;
.product-intro {
font-size: 13px;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
line-height: 28px;
height: 28px;
}
.ant-divider {
margin: 8px 0;
}
}
.duration-label {
width: 32px;
}
.duration-list {
display: flex;
flex-wrap: wrap;
.duration-item {
width: 50px;
border: 1px solid #cdcdcd;
@@ -0,0 +1,75 @@
import { request } from "/src/api/service";
const apiPrefix = "/suite/trade";
export async function GetList(query: any) {
return await request({
url: apiPrefix + "/page",
method: "post",
data: query
});
}
export async function AddObj(obj: any) {
return await request({
url: apiPrefix + "/add",
method: "post",
data: obj
});
}
export async function UpdateObj(obj: any) {
return await request({
url: apiPrefix + "/update",
method: "post",
data: obj
});
}
export async function DelObj(id: any) {
return await request({
url: apiPrefix + "/delete",
method: "post",
params: { id }
});
}
export async function GetObj(id: any) {
return await request({
url: apiPrefix + "/info",
method: "post",
params: { id }
});
}
export async function GetDetail(id: any) {
return await request({
url: apiPrefix + "/detail",
method: "post",
params: { id }
});
}
export async function DeleteBatch(ids: any[]) {
return await request({
url: apiPrefix + "/deleteByIds",
method: "post",
data: { ids }
});
}
export async function SetDefault(id: any) {
return await request({
url: apiPrefix + "/setDefault",
method: "post",
data: { id }
});
}
export async function SetDisabled(id: any, disabled: boolean) {
return await request({
url: apiPrefix + "/setDisabled",
method: "post",
data: { id, disabled }
});
}
@@ -0,0 +1,194 @@
import * as api from "./api";
import { useI18n } from "vue-i18n";
import { computed, Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, utils } from "@fast-crud/fast-crud";
import { useUserStore } from "/@/store/modules/user";
import { useSettingStore } from "/@/store/modules/settings";
import { Modal } from "ant-design-vue";
import PriceInput from "/@/views/sys/suite/product/price-input.vue";
import SuiteValue from "/@/views/sys/suite/product/suite-value.vue";
import DurationValue from "/@/views/sys/suite/product/duration-value.vue";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
const { t } = useI18n();
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
const res = await api.UpdateObj(form);
return res;
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
const res = await api.AddObj(form);
return res;
};
const userStore = useUserStore();
const settingStore = useSettingStore();
const selectedRowKeys: Ref<any[]> = ref([]);
context.selectedRowKeys = selectedRowKeys;
return {
crudOptions: {
settings: {
plugins: {
//这里使用行选择插件,生成行选择crudOptions配置,最终会与crudOptions合并
rowSelection: {
enabled: true,
order: -2,
before: true,
// handle: (pluginProps,useCrudProps)=>CrudOptions,
props: {
multiple: true,
crossPage: true,
selectedRowKeys
}
}
}
},
request: {
pageRequest,
addRequest,
editRequest,
delRequest
},
rowHandle: {
width: 100,
fixed: "right",
buttons: {
edit: { show: false },
copy: { show: false }
}
},
tabs: {
name: "status",
show: true
},
columns: {
id: {
title: "ID",
key: "id",
type: "number",
column: {
width: 100
},
form: {
show: false
}
},
tradeNo: {
title: "订单号",
type: "text",
search: { show: true },
form: {
show: false
},
column: {
width: 250
}
},
title: {
title: "商品名称",
type: "text",
search: { show: true },
column: {
width: 150
}
},
duration: {
title: "时长",
type: "number",
column: {
width: 100,
component: {
name: DurationValue,
vModel: "modelValue"
}
}
},
amount: {
title: "金额",
type: "number",
column: {
width: 100,
component: {
name: PriceInput,
vModel: "modelValue",
edit: false
}
}
},
status: {
title: "状态",
search: { show: true },
type: "dict-select",
dict: dict({
data: [
{ label: "待支付", value: "wait_pay", color: "warning" },
{ label: "已支付", value: "paid", color: "success" },
{ label: "已取消", value: "canceled", color: "error" }
]
}),
column: {
width: 100
}
},
payType: {
title: "支付方式",
search: { show: true },
type: "dict-select",
dict: dict({
data: [
{ label: "聚合支付", value: "yizhifu" },
{ label: "支付宝", value: "alipay" },
{ label: "微信", value: "wxpay" }
]
}),
column: {
width: 100,
component: {
color: "auto"
}
}
},
payTime: {
title: "支付时间",
type: "datetime",
column: {
width: 160
}
},
createTime: {
title: "创建时间",
type: "datetime",
form: {
show: false
},
column: {
sorter: true,
width: 160,
align: "center"
}
},
updateTime: {
title: "更新时间",
type: "datetime",
form: {
show: false
},
column: {
show: true,
width: 160
}
}
}
}
};
}
@@ -0,0 +1,51 @@
<template>
<fs-page class="page-cert">
<template #header>
<div class="title">我的订单</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-left>
<a-tooltip title="批量删除">
<fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button>
</a-tooltip>
</template>
</fs-crud>
</fs-page>
</template>
<script lang="ts" setup>
import { onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "./api";
defineOptions({
name: "MyTrade"
});
const { crudBinding, crudRef, crudExpose, context } = useFs({ createCrudOptions });
const selectedRowKeys = context.selectedRowKeys;
const handleBatchDelete = () => {
if (selectedRowKeys.value?.length > 0) {
Modal.confirm({
title: "确认",
content: `确定要批量删除这${selectedRowKeys.value.length}条记录吗`,
async onOk() {
await DeleteBatch(selectedRowKeys.value);
message.info("删除成功");
crudExpose.doRefresh();
selectedRowKeys.value = [];
}
});
} else {
message.error("请先勾选记录");
}
};
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
</script>
<style lang="less"></style>
@@ -12,14 +12,19 @@
<div>
<span>您好{{ userInfo.nickName || userInfo.username }} 欢迎使用 {{ siteInfo.title }}</span>
</div>
<div>
<a-tag color="green" class="flex-inline pointer"> <fs-icon icon="ion:time-outline" class="mr-5"></fs-icon> {{ now }}</a-tag>
<a-badge v-if="userStore.isAdmin" :dot="hasNewVersion">
<a-tag color="blue" class="flex-inline pointer" :title="'最新版本:' + latestVersion" @click="openUpgradeUrl()">
<fs-icon icon="ion:rocket-outline" class="mr-5"></fs-icon>
v{{ version }}
</a-tag>
</a-badge>
<div class="flex-o">
<a-tag color="green" class="flex-inline pointer m-0"> <fs-icon icon="ion:time-outline"></fs-icon> {{ now }}</a-tag>
<template v-if="userStore.isAdmin">
<a-divider type="vertical" />
<a-badge :dot="hasNewVersion">
<a-tag color="blue" class="flex-inline pointer m-0" :title="'最新版本:' + latestVersion" @click="openUpgradeUrl()">
<fs-icon icon="ion:rocket-outline" class="mr-5"></fs-icon>
v{{ version }}
</a-tag>
</a-badge>
</template>
<a-divider type="vertical" />
<suite-card class="m-0"></suite-card>
</div>
</div>
</div>
@@ -82,7 +87,7 @@
<div v-if="pluginGroups" class="plugin-list">
<a-card>
<template #title>
支持的部署任务列表 <a-tag color="green">{{ pluginGroups.groups.all.plugins.length }}</a-tag>
支持的部署任务总览 <a-tag color="green">{{ pluginGroups.groups.all.plugins.length }}</a-tag>
</template>
<a-row :gutter="10">
<a-col v-for="item of pluginGroups.groups.all.plugins" :key="item.name" class="plugin-item-col" :span="4">
@@ -118,7 +123,7 @@ import TutorialButton from "/@/components/tutorial/index.vue";
import DayCount from "./charts/day-count.vue";
import PieCount from "./charts/pie-count.vue";
import ExpiringList from "./charts/expiring-list.vue";
import SuiteCard from "./suite-card.vue";
import { useSettingStore } from "/@/store/modules/settings";
import { SiteInfo } from "/@/api/modules/api.basic";
import { UserInfoRes } from "/@/api/modules/api.user";
@@ -0,0 +1,77 @@
<template>
<div class="my-suite-card">
<div class="flex-o">
<a-popover>
<template #content>
<div>
<div class="flex-between mt-5">
<div class="flex-o"><fs-icon icon="ant-design:check-outlined" class="color-green mr-5" /> 流水线条数</div>
<suite-value :model-value="detail.pipelineCount.max" :used="detail.pipelineCount.used" unit="条" />
</div>
<div class="flex-between mt-5">
<div class="flex-o"><fs-icon icon="ant-design:check-outlined" class="color-green mr-5" />域名数量</div>
<suite-value :model-value="detail.domainCount.max" :used="detail.domainCount.used" unit="个" />
</div>
<div class="flex-between mt-5">
<div class="flex-o"><fs-icon icon="ant-design:check-outlined" class="color-green mr-5" /> 部署次数</div>
<suite-value :model-value="detail.deployCount.max" :used="detail.deployCount.used" unit="次" />
</div>
<div class="flex-between mt-5">
<div class="flex-o"><fs-icon icon="ant-design:check-outlined" class="color-green mr-5" /> 监控站点数</div>
<suite-value :model-value="detail.monitorCount.max" :used="detail.monitorCount.used" unit="次" />
</div>
</div>
</template>
<div class="flex-o">
<fs-icon icon="ant-design:gift-outlined" class="color-green mr-5" />
<a-tag v-for="(item, index) of detail.suites" :key="index" color="green" class="pointer flex-o">
<span class="mr-5">
{{ item.title }}
</span>
<span>(<expires-time-text :value="item.expiresTime" />)</span>
</a-tag>
</div>
</a-popover>
</div>
</div>
</template>
<script lang="ts" setup>
import SuiteValue from "/@/views/sys/suite/product/suite-value.vue";
import { computed, ref } from "vue";
import { request } from "/@/api/service";
import dayjs from "dayjs";
import ExpiresTimeText from "/@/components/expires-time-text.vue";
defineOptions({
name: "SuiteCard"
});
type SuiteValue = {
max: number;
used: number;
};
type SuiteDetail = {
suites?: any[];
expiresTime?: number;
pipelineCount?: SuiteValue;
domainCount?: SuiteValue;
deployCount?: SuiteValue;
monitorCount?: SuiteValue;
};
const detail = ref<SuiteDetail>({});
const api = {
async SuiteDetailGet() {
return await request({
url: "/mine/suite/detail",
method: "post"
});
}
};
async function loadSuiteDetail() {
detail.value = await api.SuiteDetailGet();
}
loadSuiteDetail();
</script>
@@ -11,6 +11,9 @@
<a-tab-pane key="register" tab="注册设置">
<SettingRegister v-if="activeKey === 'register'" />
</a-tab-pane>
<a-tab-pane key="payment" tab="支付设置">
<SettingPayment v-if="activeKey === 'payment'" />
</a-tab-pane>
</a-tabs>
</div>
</fs-page>
@@ -19,6 +22,7 @@
<script setup lang="tsx">
import SettingBase from "/@/views/sys/settings/tabs/base.vue";
import SettingRegister from "/@/views/sys/settings/tabs/register.vue";
import SettingPayment from "/@/views/sys/settings/tabs/payment.vue";
import { useRoute, useRouter } from "vue-router";
import { ref } from "vue";
defineOptions({
@@ -0,0 +1,91 @@
<template>
<div class="sys-settings-form sys-settings-payment">
<a-form ref="formRef" :model="formState" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off">
<div>支付方式</div>
<a-form-item label="易支付" :name="['yizhifu', 'enabled']" :required="true">
<a-switch v-model:checked="formState.yizhifu.enabled" />
</a-form-item>
<a-form-item v-if="formState.yizhifu.enabled" label="易支付配置" :name="['yizhifu', 'accessId']" :required="true">
<access-selector v-model="formState.yizhifu.accessId" type="yizhifu" from="sys" />
</a-form-item>
<a-form-item label="支付宝" :name="['alipay', 'enabled']" :required="true">
<a-switch v-model:checked="formState.alipay.enabled" />
</a-form-item>
<a-form-item v-if="formState.alipay.enabled" label="支付宝配置" :name="['alipay', 'accessId']" :required="true">
<access-selector v-model="formState.alipay.accessId" type="alipay" from="sys" />
</a-form-item>
<a-form-item label="微信支付" :name="['wxpay', 'enabled']" :required="true">
<a-switch v-model:checked="formState.wxpay.enabled" />
</a-form-item>
<a-form-item v-if="formState.wxpay.enabled" label="微信支付配置" :name="['wxpay', 'accessId']" :required="true">
<access-selector v-model="formState.wxpay.accessId" type="wxpay" from="sys" />
</a-form-item>
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
<loading-button type="primary" html-type="button" :click="onClick">保存</loading-button>
</a-form-item>
</a-form>
</div>
</template>
<script setup lang="tsx">
import { reactive, ref } from "vue";
import { merge } from "lodash-es";
import { notification } from "ant-design-vue";
import { request } from "/@/api/service";
defineOptions({
name: "SettingPayment"
});
const api = {
async SettingGet() {
return await request({
url: "/sys/settings/payment/get",
method: "post"
});
},
async SettingSave(data: any) {
return await request({
url: "/sys/settings/payment/save",
method: "post",
data
});
}
};
const formRef = ref<any>(null);
type PaymentItem = {
enabled: boolean;
accessId?: number;
};
const formState = reactive<
Partial<{
yizhifu: PaymentItem;
alipay: PaymentItem;
wxpay: PaymentItem;
}>
>({});
async function loadSettings() {
const data: any = await api.SettingGet();
merge(formState, data);
}
loadSettings();
const onClick = async () => {
const form = await formRef.value.validateFields();
await api.SettingSave(form);
await loadSettings();
notification.success({
message: "保存成功"
});
};
</script>
<style lang="less">
.sys-settings-base {
}
</style>
@@ -1,194 +0,0 @@
import { ColumnCompositionProps, compute, dict } from "@fast-crud/fast-crud";
import { computed, provide, ref, toRef } from "vue";
import { useReference } from "/@/use/use-refrence";
import { forEach, get, merge, set } from "lodash-es";
import { Modal } from "ant-design-vue";
import * as api from "/@/views/sys/cname/provider/api";
import { mitter } from "/@/utils/util.mitt";
export function getCommonColumnDefine(crudExpose: any, typeRef: any, api: any) {
const notificationTypeDictRef = dict({
url: "/sys/suite/payment/getList"
});
const defaultPluginConfig = {
component: {
name: "a-input",
vModel: "value"
}
};
function buildDefineFields(define: any, form: any, mode: string) {
const formWrapperRef = crudExpose.getFormWrapperRef();
const columnsRef = toRef(formWrapperRef.formOptions, "columns");
for (const key in columnsRef.value) {
if (key.indexOf(".") >= 0) {
delete columnsRef.value[key];
}
}
console.log('crudBinding.value[mode + "Form"].columns', columnsRef.value);
forEach(define.input, (value: any, mapKey: any) => {
const key = "body." + mapKey;
const field = {
...value,
key
};
const column = merge({ title: key }, defaultPluginConfig, field);
//eval
useReference(column);
if (column.required) {
if (!column.rules) {
column.rules = [];
}
column.rules.push({ required: true, message: "此项必填" });
}
//设置默认值
if (column.value != null && get(form, key) == null) {
set(form, key, column.value);
}
//字段配置赋值
columnsRef.value[key] = column;
console.log("form", columnsRef.value, form);
});
}
const currentDefine = ref();
return {
id: {
title: "ID",
key: "id",
type: "number",
column: {
width: 100
},
form: {
show: false
}
},
type: {
title: "支付类型",
type: "dict-select",
dict: notificationTypeDictRef,
search: {
show: false
},
column: {
width: 200,
component: {
color: "auto"
}
},
editForm: {
component: {
disabled: false
}
},
form: {
component: {
disabled: false,
showSearch: true,
filterOption: (input: string, option: any) => {
input = input?.toLowerCase();
return option.value.toLowerCase().indexOf(input) >= 0 || option.label.toLowerCase().indexOf(input) >= 0;
},
renderLabel(item: any) {
return (
<span class={"flex-o flex-between"}>
{item.label}
{item.needPlus && <fs-icon icon={"mingcute:vip-1-line"} className={"color-plus"}></fs-icon>}
</span>
);
}
},
rules: [{ required: true, message: "请选择通知类型" }],
valueChange: {
immediate: true,
async handle({ value, mode, form, immediate }) {
if (value == null) {
return;
}
const lastTitle = currentDefine.value?.title;
const define = await api.GetProviderDefine(value);
currentDefine.value = define;
console.log("define", define);
if (!immediate) {
form.body = {};
if (define.needPlus) {
mitter.emit("openVipModal");
}
}
if (!form.name || form.name === lastTitle) {
form.name = define.title;
}
buildDefineFields(define, form, mode);
}
},
helper: computed(() => {
const define = currentDefine.value;
if (define == null) {
return "";
}
return define.desc;
})
}
} as ColumnCompositionProps,
name: {
title: "通知名称",
search: {
show: true
},
type: ["text"],
form: {
rules: [{ required: true, message: "请填写名称" }],
helper: "随便填,当多个相同类型的通知时,便于区分"
},
column: {
width: 200
}
},
test: {
title: "测试",
form: {
show: compute(({ form }) => {
return !!form.type;
}),
component: {
name: "api-test",
action: "TestRequest"
},
order: 990,
col: {
span: 24
}
},
column: {
show: false
}
},
setting: {
column: { show: false },
form: {
show: false,
valueBuilder({ value, form }) {
form.body = {};
if (!value) {
return;
}
const setting = JSON.parse(value);
for (const key in setting) {
form.body[key] = setting[key];
}
},
valueResolve({ form }) {
const setting = form.body;
form.setting = JSON.stringify(setting);
}
}
} as ColumnCompositionProps
};
}
@@ -1,54 +0,0 @@
import { ref } from "vue";
import { getCommonColumnDefine } from "./common";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { createNotificationApi } from "/@/views/certd/notification/api";
const api = createNotificationApi();
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const editRequest = async (req: EditReq) => {
const { form, row } = req;
form.id = row.id;
const res = await api.UpdateObj(form);
return res;
};
const delRequest = async (req: DelReq) => {
const { row } = req;
return await api.DelObj(row.id);
};
const addRequest = async (req: AddReq) => {
const { form } = req;
const res = await api.AddObj(form);
return res;
};
const typeRef = ref();
const commonColumnsDefine = getCommonColumnDefine(crudExpose, typeRef, api);
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest
},
form: {
labelCol: {
//固定label宽度
span: null,
style: {
width: "145px"
}
}
},
rowHandle: {
width: 200
},
columns: {
...commonColumnsDefine
}
}
};
}
@@ -1,41 +0,0 @@
<template>
<fs-page>
<template #header>
<div class="title">
支付方式管理
<span class="sub">管理支付方式</span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</fs-page>
</template>
<script lang="ts">
import { defineComponent, onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { createPaymentApi } from "./api";
import { notificationProvide } from "/@/views/certd/notification/common";
export default defineComponent({
name: "PaymentManager",
setup() {
const api = createPaymentApi();
notificationProvide(api);
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: { api } });
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
return {
crudBinding,
crudRef
};
}
});
</script>
@@ -1,163 +0,0 @@
<template>
<div class="notification-selector">
<div class="flex-o w-100">
<fs-dict-select
class="flex-1"
:value="modelValue"
:dict="optionsDictRef"
:disabled="disabled"
:render-label="renderLabel"
:slots="selectSlots"
:allow-clear="true"
@update:value="onChange"
/>
<fs-table-select
ref="tableSelectRef"
class="flex-0"
:model-value="modelValue"
:dict="optionsDictRef"
:create-crud-options="createCrudOptions"
:crud-options-override="{
search: { show: false },
table: {
scroll: {
x: 540
}
}
}"
:show-current="false"
:show-select="false"
:dialog="{ width: 960 }"
:destroy-on-close="false"
height="400px"
@update:model-value="onChange"
@dialog-closed="doRefresh"
>
<template #default="scope">
<fs-button class="ml-5" :disabled="disabled" :size="size" type="primary" icon="ant-design:edit-outlined" @click="scope.open"></fs-button>
</template>
</fs-table-select>
</div>
</div>
</template>
<script lang="tsx" setup>
import { inject, ref, Ref, watch } from "vue";
import { createNotificationApi } from "../api";
import { message } from "ant-design-vue";
import { dict } from "@fast-crud/fast-crud";
import createCrudOptions from "../crud";
import { notificationProvide } from "/@/views/certd/notification/common";
defineOptions({
name: "NotificationSelector"
});
const props = defineProps<{
modelValue?: number | string;
type?: string;
placeholder?: string;
size?: string;
disabled?: boolean;
}>();
const onChange = async (value: number) => {
await emitValue(value);
};
const emit = defineEmits(["update:modelValue", "selectedChange", "change"]);
const api = createNotificationApi();
notificationProvide(api);
// const types = ref({});
// async function loadNotificationTypes() {
// const types = await api.GetDefineTypes();
// const map: any = {};
// for (const item of types) {
// map[item.type] = item;
// }
// types.value = map;
// }
// loadNotificationTypes();
const tableSelectRef = ref();
const optionsDictRef = dict({
url: "/pi/notification/options",
value: "id",
label: "name",
onReady: ({ dict }) => {
const data = [
{
id: 0,
name: "使用默认通知",
icon: "ion:notifications"
},
...dict.data
];
dict.setData(data);
}
});
const renderLabel = (option: any) => {
return <span>{option.name}</span>;
};
async function openTableSelectDialog(e: any) {
e.preventDefault();
await tableSelectRef.value.open();
await tableSelectRef.value.crudExpose.openAdd({});
}
const selectSlots = ref({
dropdownRender({ menuNode }: any) {
const res = [];
res.push(menuNode);
res.push(<a-divider style="margin: 4px 0" />);
res.push(<a-space style="padding: 4px 8px" />);
res.push(<fs-button class="w-100" type="text" icon="plus-outlined" text="新建通知渠道" onClick={openTableSelectDialog}></fs-button>);
return res;
}
});
const target: Ref<any> = ref({});
function clear() {
if (props.disabled) {
return;
}
emitValue(null);
}
async function emitValue(value: any) {
target.value = optionsDictRef.dataMap[value];
if (value !== 0 && pipeline?.value && target && pipeline.value.userId !== target.value.userId) {
message.error("对不起,您不能修改他人流水线的通知");
return;
}
emit("change", value);
emit("update:modelValue", value);
}
watch(
() => {
return props.modelValue;
},
async (value) => {
await optionsDictRef.loadDict();
target.value = optionsDictRef.dataMap[value];
emit("selectedChange", target.value);
},
{
immediate: true
}
);
//当不在pipeline中编辑时,可能为空
const pipeline = inject("pipeline", null);
async function doRefresh() {
await optionsDictRef.reloadDict();
}
</script>
<style lang="less">
.notification-selector {
width: 100%;
}
</style>
@@ -1,17 +1,12 @@
import * as api from "./api";
import { useI18n } from "vue-i18n";
import { Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { useUserStore } from "/@/store/modules/user";
import { useSettingStore } from "/@/store/modules/settings";
import SuiteValue from "./suite-value.vue";
import SuiteValueEdit from "./suite-value-edit.vue";
import PriceEdit from "./price-edit.vue";
import DurationPriceValue from "/@/views/sys/suite/product/duration-price-value.vue";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
const { t } = useI18n();
const emit = context.emit;
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
@@ -29,35 +24,26 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
return res;
};
const userStore = useUserStore();
const settingStore = useSettingStore();
const selectedRowKeys: Ref<any[]> = ref([]);
context.selectedRowKeys = selectedRowKeys;
return {
crudOptions: {
settings: {
plugins: {
//这里使用行选择插件,生成行选择crudOptions配置,最终会与crudOptions合并
rowSelection: {
enabled: true,
order: -2,
before: true,
// handle: (pluginProps,useCrudProps)=>CrudOptions,
props: {
multiple: true,
crossPage: true,
selectedRowKeys
}
}
table: {
onRefreshed: () => {
emit("refreshed");
}
},
search: {
show: false
},
request: {
pageRequest,
addRequest,
editRequest,
delRequest
},
pagination: {
show: false,
pageSize: 999999
},
rowHandle: {
minWidth: 200,
fixed: "right"
@@ -71,7 +57,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
content: {
header: "套餐内容",
columns: ["content.maxDomainCount", "content.maxPipelineCount", "content.maxDeployCount", "content.siteMonitor"]
columns: ["content.maxDomainCount", "content.maxPipelineCount", "content.maxDeployCount", "content.maxMonitorCount"]
},
price: {
header: "价格",
@@ -81,17 +67,17 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}
},
columns: {
id: {
title: "ID",
key: "id",
type: "number",
column: {
width: 100
},
form: {
show: false
}
},
// id: {
// title: "ID",
// key: "id",
// type: "number",
// column: {
// width: 100
// },
// form: {
// show: false
// }
// },
title: {
title: "套餐名称",
type: "text",
@@ -123,7 +109,8 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
rules: [{ required: true, message: "此项必填" }]
},
column: {
width: 100
width: 80,
align: "center"
},
valueBuilder: ({ row }) => {
if (row.content) {
@@ -205,37 +192,25 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}
}
},
"content.siteMonitor": {
title: "支持证书监控",
type: "dict-switch",
dict: dict({
data: [
{ label: "是", value: true, color: "success" },
{ label: "否", value: false, color: "error" }
]
}),
"content.maxMonitorCount": {
title: "证书监控数量",
type: "text",
form: {
key: ["content", "siteMonitor"],
value: false
key: ["content", "maxMonitorCount"],
component: {
name: SuiteValueEdit,
vModel: "modelValue",
unit: "个"
},
rules: [{ required: true, message: "此项必填" }]
},
column: {
width: 120
}
},
isBootstrap: {
title: "是否初始套餐",
type: "dict-switch",
dict: dict({
data: [
{ label: "是", value: true, color: "success" },
{ label: "否", value: false, color: "gray" }
]
}),
form: {
value: false
},
column: {
width: 120
width: 120,
component: {
name: SuiteValue,
vModel: "modelValue",
unit: "个"
}
}
},
durationPrices: {
@@ -258,10 +233,26 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
column: {
component: {
name: PriceEdit,
vModel: "modelValue",
edit: false
}
name: DurationPriceValue,
vModel: "modelValue"
},
width: 350
}
},
supportBuy: {
title: "支持购买",
type: "dict-switch",
dict: dict({
data: [
{ label: "是", value: true, color: "success" },
{ label: "否", value: false, color: "gray" }
]
}),
form: {
value: false
},
column: {
width: 120
}
},
disabled: {
@@ -0,0 +1,36 @@
<template>
<div class="cd-duration-price-value">
<a-tag v-for="item of modelValue" :key="item.duration" class="flex-o price-group-item m-2">
<div style="width: 40px">{{ durationDict.dataMap[item.duration]?.label }}:</div>
<price-input v-model="item.price" :edit="false" class="mr-5" />
</a-tag>
</div>
</template>
<script lang="ts" setup>
import { PriceItem } from "./api";
import PriceInput from "./price-input.vue";
import { durationDict } from "../../../certd/suite/api";
defineOptions({
name: "DurationPriceValue"
});
const props = withDefaults(
defineProps<{
modelValue?: PriceItem[];
}>(),
{
modelValue: () => {
return [];
}
}
);
</script>
<style lang="less">
.cd-duration-price-value {
display: flex;
flex-wrap: wrap;
item-align: center;
}
</style>
@@ -0,0 +1,11 @@
<template>
<a-tag color="green"> {{ durationDict.dataMap[modelValue]?.label }}</a-tag>
</template>
<script lang="ts" setup>
import { durationDict } from "/@/views/certd/suite/api";
const props = defineProps<{
modelValue: number;
}>();
</script>
@@ -1,50 +1,19 @@
<template>
<fs-page class="page-cert">
<template #header>
<div class="title">
套餐管理
<span class="sub"> 必须设置一个初始套餐 </span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-left>
<a-tooltip title="批量删除">
<fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button>
</a-tooltip>
</template>
</fs-crud>
</fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</template>
<script lang="ts" setup>
import { onMounted } from "vue";
import { defineEmits, onMounted, ref } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "./api";
defineOptions({
name: "ProductManager"
});
const { crudBinding, crudRef, crudExpose, context } = useFs({ createCrudOptions });
const emit = defineEmits(["refreshed"]);
const selectedRowKeys = context.selectedRowKeys;
const handleBatchDelete = () => {
if (selectedRowKeys.value?.length > 0) {
Modal.confirm({
title: "确认",
content: `确定要批量删除这${selectedRowKeys.value.length}条记录吗`,
async onOk() {
await DeleteBatch(selectedRowKeys.value);
message.info("删除成功");
crudExpose.doRefresh();
selectedRowKeys.value = [];
}
});
} else {
message.error("请先勾选记录");
}
};
const context: any = { emit };
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context });
// 页面打开后获取列表数据
onMounted(() => {
@@ -10,7 +10,7 @@ import { computed } from "vue";
const props = defineProps<{
modelValue?: number;
edit: boolean;
edit?: boolean;
}>();
const priceValue = computed(() => {
@@ -1,6 +1,9 @@
<template>
<div v-if="target" class="cd-suite-value">
<a-tag :color="target.color" class="m-0">{{ target.label }}</a-tag>
<a-tag :color="target.color" class="m-0">
<span v-if="used != null">{{ used }} /</span>
{{ target.label }}
</a-tag>
</div>
</template>
@@ -10,6 +13,7 @@ import { computed } from "vue";
const props = defineProps<{
modelValue: number;
unit?: string;
used?: number;
}>();
const target = computed(() => {
@@ -29,10 +33,14 @@ const target = computed(() => {
color: "red"
};
} else {
let color = "blue";
if (props.used != null) {
color = props.used >= props.modelValue ? "red" : "green";
}
return {
value: props.modelValue,
label: props.modelValue + (props.unit || ""),
color: "blue"
color: color
};
}
});
@@ -0,0 +1,110 @@
<template>
<fs-page class="page-sys-settings page-sys-settings-suite">
<template #header>
<div class="title">
套餐设置
<span class="sub"> 需要<router-link to="/sys/settings" :query="{ tab: 'payment' }">开启至少一种支付方式</router-link></span>
</div>
</template>
<div class="form-content">
<a-form ref="formRef" :model="formState" :label-col="{ style: { width: '150px' } }" :wrapper-col="{ span: 20 }" autocomplete="off">
<a-form-item label="开启套餐功能" name="enabled" required>
<a-switch v-model:checked="formState.enabled" />
</a-form-item>
<template v-if="formState.enabled">
<a-form-item label="套餐列表" name="enabled">
<div style="height: 400px">
<ProductManager @refreshed="onTableRefresh"></ProductManager>
</div>
</a-form-item>
<a-form-item label="注册赠送套餐" name="registerGift">
<suite-duration-selector ref="suiteDurationSelectedRef" v-model="formState.registerGift"></suite-duration-selector>
<div class="helper">添加套餐后再选择</div>
</a-form-item>
<a-form-item label="套餐说明" name="intro">
<a-textarea v-model:value="formState.intro" :rows="3"></a-textarea>
<div class="helper">将显示在套餐购买页面</div>
</a-form-item>
</template>
<a-form-item label=" " :colon="false">
<loading-button type="primary" html-type="button" :click="onClick">保存</loading-button>
</a-form-item>
</a-form>
</div>
</fs-page>
</template>
<script lang="ts" setup>
import { reactive, ref } from "vue";
import { merge } from "lodash-es";
import { notification } from "ant-design-vue";
import { request } from "/@/api/service";
import SuiteDurationSelector from "/@/views/sys/suite/setting/suite-duration-selector.vue";
import ProductManager from "/@/views/sys/suite/product/index.vue";
defineOptions({
name: "SettingsSuite"
});
const api = {
async SuiteSettingGet() {
return await request({
url: "/sys/settings/suite/get",
method: "post"
});
},
async SuiteSettingSave(data: any) {
return await request({
url: "/sys/settings/suite/save",
method: "post",
data
});
}
};
const formRef = ref<any>(null);
const formState = reactive<
Partial<{
enabled: boolean;
registerGift?: {
productId?: number;
duration?: number;
};
intro?: string;
}>
>({ enabled: false });
async function loadSettings() {
const data: any = await api.SuiteSettingGet();
merge(formState, data);
}
loadSettings();
const onClick = async () => {
const form = await formRef.value.validateFields();
await api.SuiteSettingSave(form);
await loadSettings();
notification.success({
message: "保存成功"
});
};
const suiteDurationSelectedRef = ref();
function onTableRefresh() {
suiteDurationSelectedRef.value?.refresh();
}
</script>
<style lang="less">
.page-sys-settings-suite {
.form-content {
padding: 20px;
.ant-table-body {
height: 400px !important;
}
}
}
</style>
@@ -0,0 +1,85 @@
<template>
<fs-dict-select style="width: 200px" :value="selectedValue" :dict="suiteDictRef" @selected-change="onSelectedChange"></fs-dict-select>
</template>
<script setup lang="ts">
import { durationDict } from "/@/views/certd/suite/api";
import { ref, watch } from "vue";
import { dict } from "@fast-crud/fast-crud";
import { request } from "/@/api/service";
const props = defineProps<{
modelValue?: {
productId?: number;
duration?: number;
};
}>();
const suiteDictRef = dict({
async getData() {
const res = await request({
url: "/sys/suite/product/list",
method: "post"
});
const options: any = [
{
value: "",
label: "不赠送"
}
];
res.forEach((item: any) => {
const durationPrices = JSON.parse(item.durationPrices);
for (const dp of durationPrices) {
const value = item.id + "_" + dp.duration;
options.push({
label: `${item.title}<${durationDict.dataMap[dp.duration]?.label}>`,
value: value,
target: {
productId: item.id,
duration: dp.duration
}
});
}
});
return options;
}
});
const selectedValue = ref();
watch(
() => {
return props.modelValue;
},
(value) => {
if (value && value.productId && value.duration) {
selectedValue.value = value.productId + "_" + value.duration;
} else {
selectedValue.value = "";
}
},
{
immediate: true
}
);
const emit = defineEmits(["update:modelValue"]);
const onSelectedChange = (value: any) => {
selectedValue.value = value;
if (!value) {
emit("update:modelValue", undefined);
return;
}
const arr = value.value.split("_");
emit("update:modelValue", {
productId: parseInt(arr[0]),
duration: parseInt(arr[1])
});
};
defineExpose({
refresh() {
suiteDictRef.reloadDict();
}
});
</script>
<style lang="less"></style>