This commit is contained in:
孟帅
2023-05-10 23:54:50 +08:00
parent bbe655a4d8
commit 49a96750bf
314 changed files with 15138 additions and 6244 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "hotgo",
"version": "2.5.3",
"version": "2.6.7",
"author": {
"name": "MengShuai",
"email": "133814250@qq.com",
@@ -56,7 +56,8 @@
"vue-router": "^4.0.15",
"vue-types": "^4.1.1",
"vue3-json-viewer": "^2.2.2",
"vuedraggable": "^4.1.0"
"vuedraggable": "^4.1.0",
"weixin-js-sdk": "^1.6.0"
},
"devDependencies": {
"@commitlint/cli": "^12.1.4",

41
web/src/api/cash/index.ts Normal file
View File

@@ -0,0 +1,41 @@
import { http } from '@/utils/http/axios';
export function List(params) {
return http.request({
url: '/cash/list',
method: 'get',
params,
});
}
export function Edit(params) {
return http.request({
url: '/cash/edit',
method: 'POST',
params,
});
}
export function View(params) {
return http.request({
url: '/cash/view',
method: 'GET',
params,
});
}
export function Apply(params) {
return http.request({
url: '/cash/apply',
method: 'POST',
params,
});
}
export function Payment(params) {
return http.request({
url: '/cash/payment',
method: 'POST',
params,
});
}

View File

@@ -0,0 +1,23 @@
import { http, jumpExport } from '@/utils/http/axios';
// 获取资产变动列表
export function List(params) {
return http.request({
url: '/creditsLog/list',
method: 'get',
params,
});
}
// 导出资产变动
export function Export(params) {
jumpExport('/creditsLog/export', params);
}
// 获取变动状态选项
export function Option() {
return http.request({
url: '/creditsLog/option',
method: 'GET',
});
}

105
web/src/api/order/index.ts Normal file
View File

@@ -0,0 +1,105 @@
import { http, jumpExport } from '@/utils/http/axios';
// 创建充值订单
export function Create(params) {
return http.request({
url: '/order/create',
method: 'post',
params,
});
}
// 获取充值订单列表
export function List(params) {
return http.request({
url: '/order/list',
method: 'get',
params,
});
}
// 删除/批量删除充值订单
export function Delete(params) {
return http.request({
url: '/order/delete',
method: 'POST',
params,
});
}
// 添加/编辑充值订单
export function Edit(params) {
return http.request({
url: '/order/edit',
method: 'POST',
params,
});
}
// 修改充值订单状态
export function Status(params) {
return http.request({
url: '/order/status',
method: 'POST',
params,
});
}
// 操作充值订单开关
export function Switch(params) {
return http.request({
url: '/order/switch',
method: 'POST',
params,
});
}
// 获取充值订单指定详情
export function View(params) {
return http.request({
url: '/order/view',
method: 'GET',
params,
});
}
// 获取充值订单最大排序
export function MaxSort() {
return http.request({
url: '/order/maxSort',
method: 'GET',
});
}
// 获取订单状态选项
export function Option() {
return http.request({
url: '/order/option',
method: 'GET',
});
}
// 申请订单退款
export function ApplyRefund(params) {
return http.request({
url: '/order/applyRefund',
method: 'post',
params,
});
}
// 受理订单退款
export function AcceptRefund(params) {
return http.request({
url: '/order/acceptRefund',
method: 'post',
params,
});
}
// 导出充值订单
export function Export(params) {
jumpExport('/order/export', params);
}

View File

@@ -31,3 +31,12 @@ export function Delete(params) {
params,
});
}
export function getDeptOption() {
const params = { pageSize: 100 };
return http.request({
url: '/dept/option',
method: 'GET',
params,
});
}

View File

@@ -8,6 +8,10 @@ export function getPostList(params?) {
});
}
export function getPostOption(params?) {
return getPostList(params);
}
export function Edit(params) {
return http.request({
url: '/post/edit',

View File

@@ -47,3 +47,27 @@ export function GetMemberOption() {
method: 'GET',
});
}
export function GetMemberView(params) {
return http.request({
url: '/member/view',
method: 'GET',
params,
});
}
export function AddMemberBalance(params) {
return http.request({
url: '/member/addBalance',
method: 'POST',
params,
});
}
export function AddMemberIntegral(params) {
return http.request({
url: '/member/addIntegral',
method: 'POST',
params,
});
}

15
web/src/api/pay/refund.ts Normal file
View File

@@ -0,0 +1,15 @@
import { http, jumpExport } from '@/utils/http/axios';
// 获取交易退款列表
export function List(params) {
return http.request({
url: '/payRefund/list',
method: 'get',
params,
});
}
// 导出交易退款
export function Export(params) {
jumpExport('/payRefund/export', params);
}

View File

@@ -37,3 +37,10 @@ export function sendTestSms(params) {
params,
});
}
export function getCashConfig() {
return http.request({
url: '/config/getCash',
method: 'get',
});
}

View File

@@ -11,6 +11,11 @@ export function getRoleList(params) {
});
}
export function getRoleOption() {
const params = { pageSize: 100 };
return getRoleList(params);
}
export function Edit(params) {
return http.request({
url: '/role/edit',

View File

@@ -3,6 +3,7 @@ export enum SocketEnum {
EventConnected = 'connected',
EventAdminMonitorTrends = 'admin/monitor/trends',
EventAdminMonitorRunInfo = 'admin/monitor/runInfo',
EventAdminOrderNotify = 'admin/order/notify',
TypeQueryUser = 2,
TypeBoardCastMsg = 3,
TypeQuerySwitcher = 4,

View File

@@ -1,11 +1,13 @@
import type { RouteRecordRaw } from 'vue-router';
import { isNavigationFailure, Router } from 'vue-router';
import { useUserStoreWidthOut } from '@/store/modules/user';
import { UserInfoState, useUserStoreWidthOut } from '@/store/modules/user';
import { useAsyncRouteStoreWidthOut } from '@/store/modules/asyncRoute';
import { ACCESS_TOKEN } from '@/store/mutation-types';
import { storage } from '@/utils/Storage';
import { PageEnum } from '@/enums/pageEnum';
import { ErrorPageRoute } from '@/router/base';
import { isWechatBrowser } from '@/utils/is';
import { jump } from '@/utils/http/axios';
const LOGIN_PATH = PageEnum.BASE_LOGIN;
const whitePathList = [LOGIN_PATH]; // no redirect whitelist
@@ -55,7 +57,24 @@ export function createRouterGuards(router: Router) {
return;
}
const redirectPath = (from.query.redirect || to.path) as string;
const redirect = decodeURIComponent(redirectPath);
const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect };
const userInfo = await userStore.GetInfo();
// 如果是微信访问则记录本次登录的openid
if (isWechatBrowser() && (userInfo as UserInfoState).openId === '') {
let path = nextData.path;
if (path === LOGIN_PATH) {
path = PageEnum.BASE_HOME_REDIRECT;
}
const w = window.location;
const URI = w.protocol + '//' + w.host + w.pathname + '#' + path;
jump('/wechat/authorize', { type: 'openId', syncRedirect: URI });
return;
}
await userStore.GetConfig();
const routes = await asyncRouteStore.generateRoutes(userInfo);
@@ -70,9 +89,6 @@ export function createRouterGuards(router: Router) {
router.addRoute(ErrorPageRoute as unknown as RouteRecordRaw);
}
const redirectPath = (from.query.redirect || to.path) as string;
const redirect = decodeURIComponent(redirectPath);
const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect };
asyncRouteStore.setDynamicAddedRoute(true);
next(nextData);
Loading && Loading.finish();

View File

@@ -4,7 +4,6 @@ import { store } from '@/store';
import { ACCESS_TOKEN, CURRENT_CONFIG, CURRENT_USER, IS_LOCKSCREEN } from '@/store/mutation-types';
import { ResultEnum } from '@/enums/httpEnum';
import { getConfig, getUserInfo, login } from '@/api/system/user';
const Storage = createStorage({ storage: localStorage });
export interface UserInfoState {
@@ -17,6 +16,7 @@ export interface UserInfoState {
realName: string;
avatar: string;
balance: number;
integral: number;
sex: number;
qq: string;
email: string;
@@ -33,6 +33,7 @@ export interface UserInfoState {
loginCount: number;
lastLoginAt: string;
lastLoginIp: string;
openId: string;
}
export interface ConfigState {
@@ -125,14 +126,13 @@ export const useUserStore = defineStore({
return Promise.reject(e);
}
},
// 获取用户信息
GetInfo() {
const that: any = this;
return new Promise((resolve, reject) => {
getUserInfo()
.then((res) => {
const result = res;
const result = res as UserInfoState;
if (result.permissions && result.permissions.length) {
const permissionsList = result.permissions;
that.setPermissions(permissionsList);
@@ -143,7 +143,7 @@ export const useUserStore = defineStore({
} else {
reject(new Error('getInfo: permissionsList must be a non-null array !'));
}
resolve(res);
resolve(result);
})
.catch((error) => {
reject(error);

View File

@@ -178,11 +178,15 @@ export function defShortcuts() {
export function defRangeShortcuts() {
const nowDate = new Date();
const dayBase = 86400 * 1000;
return {
: [startOfToday().getTime(), endOfToday().getTime()] as const,
: () => {
return [startOfYesterday().getTime(), endOfYesterday().getTime()] as const;
},
7: [startOfToday().getTime() - dayBase * 6, endOfToday().getTime()] as const,
30: [startOfToday().getTime() - dayBase * 29, endOfToday().getTime()] as const,
90: [startOfToday().getTime() - dayBase * 89, endOfToday().getTime()] as const,
: () => {
return [
startOfWeek(nowDate, { weekStartsOn: 1 }).getTime(),
@@ -192,7 +196,7 @@ export function defRangeShortcuts() {
: () => {
return [startOfMonth(nowDate).getTime(), endOfMonth(nowDate).getTime()] as const;
},
: () => {
: () => {
return [
startOfMonth(subMonths(nowDate, 1)).getTime(),
endOfMonth(subMonths(nowDate, 1)).getTime(),

View File

@@ -19,6 +19,7 @@ import { useUserStoreWidthOut } from '@/store/modules/user';
import router from '@/router';
import { storage } from '@/utils/Storage';
import { encodeParams } from '@/utils/urlUtils';
import { delNullProperty } from '@/utils/array';
const globSetting = useGlobSetting();
const urlPrefix = globSetting.urlPrefix || '';
@@ -287,7 +288,22 @@ export const jumpExport = function (url, params) {
urlPrefix +
url +
'?' +
encodeParams({ ...params, ...{ authorization: useUserStoreWidthOut().token } });
encodeParams({
...delNullProperty(params),
...{ authorization: useUserStoreWidthOut().token },
});
};
// 跳转
export const jump = function (url, params) {
window.location.href =
urlPrefix +
url +
'?' +
encodeParams({
...delNullProperty(params),
...{ authorization: useUserStoreWidthOut().token },
});
};
// 项目,多个不同 api 地址,直接在这里导出多个

View File

@@ -142,3 +142,8 @@ export function isLetterBegin(str) {
export function isUrl(url: string): boolean {
return /(^http|https:\/\/)/g.test(url);
}
// 判断是否为微信浏览器
export function isWechatBrowser(): boolean {
return /micromessenger/.test(navigator.userAgent.toLowerCase()) ? true : false;
}

View File

@@ -106,4 +106,4 @@ async function loadOptions() {
});
}
await loadOptions();
await loadOptions();

View File

@@ -0,0 +1,106 @@
import { h } from 'vue';
import { NTag } from 'naive-ui';
const msgMap = {
1: '处理中',
2: '提现成功',
3: ' 提现异常',
};
export const statusOptions = [
{
value: 1,
label: '处理中',
},
{
value: 2,
label: '提现成功',
},
{
value: 3,
label: '提现异常',
},
];
export const columns = [
{
title: '提现ID',
key: 'id',
width: 100,
},
{
title: '用户名',
key: 'memberUser',
render(row) {
return row.memberUser;
},
width: 100,
},
{
title: '姓名',
key: 'memberName',
render(row) {
return row.memberName;
},
width: 100,
},
{
title: '提现金额',
key: 'money',
render(row) {
return row.money.toFixed(2);
},
width: 100,
},
{
title: '手续费',
key: 'fee',
render(row) {
return row.fee.toFixed(2);
},
width: 100,
},
{
title: '最终到账',
key: 'lastMoney',
render(row) {
return row.lastMoney.toFixed(2);
},
width: 100,
},
{
title: '处理结果',
key: 'msg',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.status == 1 ? 'info' : row.status == 2 ? 'success' : 'warning',
bordered: false,
},
{
default: () => (row.msg == '' ? msgMap[row.status] : row.msg),
}
);
},
width: 200,
},
{
title: '申请IP',
key: 'ip',
width: 180,
},
{
title: '处理时间',
key: 'handleAt',
width: 180,
},
{
title: '申请时间',
key: 'createdAt',
width: 180,
},
];

View File

@@ -0,0 +1,41 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="提现管理" />
</div>
<n-card :bordered="false" class="proCard">
<n-tabs
type="card"
class="card-tabs"
:value="defaultTab"
animated
@before-leave="handleBeforeLeave"
>
<n-tab-pane name="" tab="全部"> <List :type="defaultTab" /></n-tab-pane>
<n-tab-pane name="1" tab="处理中"> <List :type="defaultTab" /> </n-tab-pane>
<n-tab-pane name="2" tab="提现成功"> <List :type="defaultTab" /> </n-tab-pane>
<n-tab-pane name="3" tab="提现异常"> <List :type="defaultTab" /> </n-tab-pane>
</n-tabs>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import List from './list.vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const defaultTab = ref('');
onMounted(() => {
if (router.currentRoute.value.query?.type) {
defaultTab.value = router.currentRoute.value.query.type as string;
}
});
function handleBeforeLeave(tabName: string) {
defaultTab.value = tabName;
}
</script>

View File

@@ -0,0 +1,415 @@
<template>
<div>
<n-card :bordered="false" class="proCard">
<BasicForm
@register="register"
@submit="handleSubmit"
@reset="handleReset"
@keyup.enter="handleSubmit"
ref="searchFormRef"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
:scroll-x="1800"
>
<template #tableTitle>
<n-button type="primary" @click="addTable" class="min-left-space">
<template #icon>
<n-icon>
<MoneyCollectOutlined />
</n-icon>
</template>
申请提现
</n-button>
<n-button type="default" @click="setCash" class="min-left-space">
<template #icon>
<n-icon>
<EditOutlined />
</n-icon>
</template>
设置提现账户
</n-button>
</template>
</BasicTable>
<n-modal
v-model:show="showModal"
:show-icon="false"
preset="dialog"
title="申请提现"
:style="{
width: dialogWidth,
}"
>
<n-alert type="info">
<div v-html="config.cashTips"></div>
</n-alert>
<n-form
:model="formParams"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="100"
class="py-4"
>
<n-form-item label="可提现金额">
<n-input v-model:value="newUserInfo.balance" disabled />
<template #feedback
><p>{{ estimated }}</p></template
>
</n-form-item>
<br />
<n-form-item label="提现金额" path="money">
<n-input-number v-model:value="formParams.money" :min="1" :max="newUserInfo.balance">
<template #minus-icon>
<n-icon :component="ArrowDownCircleOutline" />
</template>
<template #add-icon>
<n-icon :component="ArrowUpCircleOutline" />
</template>
</n-input-number>
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="() => (showModal = false)">取消</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">确定</n-button>
</n-space>
</template>
</n-modal>
<n-modal
v-model:show="showPaymentModal"
:show-icon="false"
preset="dialog"
title="处理打款"
:style="{
width: dialogWidth,
}"
>
<n-form
:model="paymentParams"
:rules="rules"
ref="PaymentRef"
label-placement="left"
:label-width="100"
class="py-4"
>
<n-form-item label="最终到账金额">
<n-input v-model:value="paymentParams.lastMoney" disabled />
</n-form-item>
<n-form-item label="收款信息">
<n-input v-model:value="paymentParams.accountInfo" disabled />
</n-form-item>
<n-form-item label="收款码">
<n-carousel draggable>
<img style="width: 200px" class="carousel-img" :src="paymentParams.payeeCode" />
</n-carousel>
</n-form-item>
<n-form-item label="提现状态" path="status">
<n-radio-group v-model:value="paymentParams.status" name="status">
<n-radio-button
v-for="status in statusOptions"
:key="status.value"
:value="status.value"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
<n-form-item label="处理结果">
<n-input v-model:value="paymentParams.msg" />
<template #feedback>不填默认显示提现状态</template>
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="() => (showPaymentModal = false)">取消</n-button>
<n-button type="info" :loading="PaymentBtnLoading" @click="confirmPayment"
>确定</n-button
>
</n-space>
</template>
</n-modal>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { Apply, List, Payment, View } from '@/api/cash';
import { columns, statusOptions } from './columns';
import { ArrowDownCircleOutline, ArrowUpCircleOutline } from '@vicons/ionicons5';
import { MoneyCollectOutlined, EditOutlined } from '@vicons/antd';
import { defRangeShortcuts, timestampToTime } from '@/utils/dateUtil';
import { useRouter } from 'vue-router';
import { getUserInfo } from '@/api/system/user';
import { getCashConfig } from '@/api/sys/config';
interface Props {
type?: string;
}
const props = withDefaults(defineProps<Props>(), {
type: '',
});
const router = useRouter();
const params = ref<any>({
pageSize: 10,
title: '',
content: '',
status: null,
});
const rules = {};
const estimated = ref(
'本次提现预计将在 ' +
timestampToTime(new Date().setTime(new Date().getTime() + 86400 * 4 * 1000) / 1000) +
' 前到账 (1-3个工作日双休日和法定节假日顺延)'
);
const schemas: FormSchema[] = [
{
field: 'memberId',
component: 'NInput',
label: '管理员ID',
componentProps: {
placeholder: '请输入管理员ID',
onUpdateValue: (e: any) => {
console.log(e);
},
},
rules: [{ message: '请输入管理员ID', trigger: ['blur'] }],
},
{
field: 'ip',
component: 'NInput',
label: '申请IP',
componentProps: {
placeholder: '请输入申请IP',
onUpdateValue: (e: any) => {
console.log(e);
},
},
rules: [{ message: '请输入申请IP', trigger: ['blur'] }],
},
{
field: 'created_at',
component: 'NDatePicker',
label: '申请时间',
componentProps: {
type: 'datetimerange',
clearable: true,
shortcuts: defRangeShortcuts(),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
];
const newUserInfo = ref({ balance: 0 });
const message = useMessage();
const actionRef = ref();
const showModal = ref(false);
const showPaymentModal = ref(false);
const PaymentRef = ref<any>({});
const PaymentBtnLoading = ref(false);
const formBtnLoading = ref(false);
const searchFormRef = ref<any>({});
const formRef = ref<any>({});
const config = ref<any>({
cashMinFee: 3,
cashMinFeeRatio: '0.03',
cashMinMoney: 0,
cashSwitch: false,
cashTips: '',
});
const resetFormParams = {
money: null,
accountInfo: null,
};
let formParams = ref<any>(resetFormParams);
const resetPaymentParams = {
id: null,
money: null,
};
let paymentParams = ref<any>(resetPaymentParams);
const actionColumn = reactive({
auth: ['/cash/payment'],
width: 100,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '处理打款',
onClick: handleEdit.bind(null, record),
},
],
});
},
});
function setCash() {
router.push({
name: 'home_account',
query: {
type: 3,
},
});
}
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
async function addTable() {
showModal.value = true;
formParams.value = resetFormParams;
newUserInfo.value = await getUserInfo();
if (newUserInfo.value.balance < config.value.cashMinMoney) {
message.error('当前余额不满足提现条件,至少需要:' + config.value.cashMinMoney + '元');
}
}
const loadDataTable = async (res) => {
mapWidth();
config.value = await getCashConfig();
config.value = config.value.list;
return await List({
...params.value,
...res,
...searchFormRef.value.formModel,
...{ status: props.type },
});
};
function reloadTable() {
actionRef.value.reload();
}
/**
* 申请提现
* @param e
*/
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
Apply({ money: formParams.value.money })
.then((_res) => {
message.success('操作成功');
setTimeout(() => {
showModal.value = false;
reloadTable();
formParams.value = ref(resetFormParams);
});
})
.catch((_e: Error) => {
// message.error(e.message ?? '操作失败');
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
/**
* 处理打款
* @param e
*/
function confirmPayment(e) {
e.preventDefault();
PaymentBtnLoading.value = true;
PaymentRef.value.validate((errors) => {
if (!errors) {
Payment({
id: PaymentRef.value.model.id,
status: PaymentRef.value.model.status,
msg: PaymentRef.value.model.msg,
})
.then((_res) => {
message.success('操作成功');
setTimeout(() => {
showPaymentModal.value = false;
reloadTable();
PaymentRef.value = ref(resetPaymentParams);
});
})
.catch((_e: Error) => {
// message.error(e.message ?? '操作失败');
});
} else {
message.error('请填写完整信息');
}
PaymentBtnLoading.value = false;
});
}
async function handleEdit(record: Recordable) {
showPaymentModal.value = true;
paymentParams.value = record;
paymentParams.value = await View({ id: record.id });
paymentParams.value.lastMoney = paymentParams.value.lastMoney.toFixed(2);
paymentParams.value.accountInfo =
paymentParams.value.name + ' - ' + paymentParams.value.account;
}
function handleSubmit(values: Recordable) {
console.log(values);
params.value = values;
reloadTable();
}
function handleReset(values: Recordable) {
params.value = values;
reloadTable();
}
const dialogWidth = ref('50%');
function mapWidth() {
let val = document.body.clientWidth;
const def = 720; // 默认宽度
if (val < def) {
dialogWidth.value = '100%';
} else {
dialogWidth.value = def + 'px';
}
return dialogWidth.value;
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,44 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="资金变动">
你和下级在平台中余额积分的变动明细都可以在这里进行查看
</n-card>
</div>
<n-card :bordered="false" class="proCard">
<n-tabs
type="card"
class="card-tabs"
:value="defaultTab"
animated
@before-leave="handleBeforeLeave"
>
<n-tab-pane name="" tab="全部"> <List :type="defaultTab" /></n-tab-pane>
<n-tab-pane name="balance" tab="余额明细"> <List :type="defaultTab" /> </n-tab-pane>
<n-tab-pane name="integral" tab="积分明细"> <List :type="defaultTab" /> </n-tab-pane>
</n-tabs>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import List from './list.vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const defaultTab = ref('');
onMounted(() => {
if (router.currentRoute.value.query?.type) {
defaultTab.value = router.currentRoute.value.query.type as string;
}
});
function handleBeforeLeave(tabName: string) {
defaultTab.value = tabName;
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,93 @@
<template>
<div>
<n-card :bordered="false" class="proCard">
<BasicForm
@register="register"
@submit="reloadTable"
@reset="reloadTable"
@keyup.enter="reloadTable"
ref="searchFormRef"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:openChecked="false"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:scroll-x="1800"
:resizeHeightOffset="-10000"
size="small"
>
<template #tableTitle>
<n-button
type="primary"
@click="handleExport"
class="min-left-space"
v-if="hasPermission(['/creditsLog/export'])"
>
<template #icon>
<n-icon>
<ExportOutlined />
</n-icon>
</template>
导出
</n-button>
</template>
</BasicTable>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { useMessage } from 'naive-ui';
import { BasicTable } from '@/components/Table';
import { BasicForm, useForm } from '@/components/Form/index';
import { usePermission } from '@/hooks/web/usePermission';
import { List, Export } from '@/api/creditsLog';
import { columns, schemas } from './model';
import { ExportOutlined } from '@vicons/antd';
interface Props {
type?: string;
}
const props = withDefaults(defineProps<Props>(), {
type: '',
});
const { hasPermission } = usePermission();
const actionRef = ref();
const message = useMessage();
const searchFormRef = ref<any>({});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
const loadDataTable = async (res) => {
return await List({
...searchFormRef.value?.formModel,
...res,
...{ creditType: props.type },
});
};
function reloadTable() {
actionRef.value.reload();
}
function handleExport() {
message.loading('正在导出列表...', { duration: 1200 });
Export(searchFormRef.value?.formModel);
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,256 @@
import { h, ref } from 'vue';
import { NTag } from 'naive-ui';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { Option } from '@/api/creditsLog';
import { isNullObject } from '@/utils/is';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { getOptionLabel, getOptionTag, Options } from '@/utils/hotgo';
export interface State {
id: number;
memberId: number;
appId: string;
addonsName: string;
creditType: string;
creditGroup: string;
beforeNum: number;
num: number;
afterNum: number;
remark: string;
ip: string;
mapId: number;
status: number;
createdAt: string;
updatedAt: string;
}
export const defaultState = {
id: 0,
memberId: 0,
appId: '',
addonsName: '',
creditType: '',
creditGroup: '',
beforeNum: 0,
num: 0,
afterNum: 0,
remark: '',
ip: '',
mapId: 0,
status: 1,
createdAt: '',
updatedAt: '',
};
export function newState(state: State | null): State {
if (state !== null) {
return cloneDeep(state);
}
return cloneDeep(defaultState);
}
export const options = ref<Options>({
creditType: [],
creditGroup: [],
});
export const rules = {};
export const schemas = ref<FormSchema[]>([
{
field: 'memberId',
component: 'NInput',
label: '管理员ID',
componentProps: {
placeholder: '请输入管理员ID',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'creditGroup',
component: 'NSelect',
label: '组别',
defaultValue: null,
componentProps: {
placeholder: '请选择变动的组别',
options: [],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'remark',
component: 'NInput',
label: '备注',
componentProps: {
placeholder: '请输入备注',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'ip',
component: 'NInput',
label: '操作人IP',
componentProps: {
placeholder: '请输入操作人IP',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'createdAt',
component: 'NDatePicker',
label: '变动时间',
componentProps: {
type: 'datetimerange',
clearable: true,
shortcuts: defRangeShortcuts(),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'id',
component: 'NInput',
label: '变动ID',
componentProps: {
placeholder: '请输入变动ID',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
]);
export const columns = [
{
title: '变动ID',
key: 'id',
width: 100,
},
{
title: '管理员ID',
key: 'memberId',
width: 100,
},
{
title: '变动类型',
key: 'creditType',
render(row) {
if (isNullObject(row.creditType)) {
return ``;
}
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: getOptionTag(options.value.creditType, row.creditType),
bordered: false,
},
{
default: () => getOptionLabel(options.value.creditType, row.creditType),
}
);
},
width: 150,
},
{
title: '组别',
key: 'creditGroup',
render(row) {
if (isNullObject(row.creditGroup)) {
return ``;
}
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: getOptionTag(options.value.creditGroup, row.creditGroup),
bordered: false,
},
{
default: () => getOptionLabel(options.value.creditGroup, row.creditGroup),
}
);
},
width: 150,
},
{
title: '变动前',
key: 'beforeNum',
width: 100,
render(row) {
return Number(row.beforeNum).toFixed(2);
},
},
{
title: '变动数量',
key: 'num',
width: 100,
render(row) {
return Number(row.num).toFixed(2);
},
},
{
title: '变动后',
key: 'afterNum',
width: 100,
render(row) {
return Number(row.afterNum).toFixed(2);
},
},
{
title: '备注',
key: 'remark',
width: 200,
},
{
title: '操作人IP',
key: 'ip',
width: 150,
},
{
title: '关联ID',
key: 'mapId',
width: 100,
render(row) {
if (row.mapId === 0) {
return '-';
}
return row.mapId;
},
},
{
title: '变动时间',
key: 'createdAt',
width: 180,
},
];
async function loadOptions() {
options.value = await Option();
for (const item of schemas.value) {
switch (item.field) {
case 'creditType':
item.componentProps.options = options.value.creditType;
break;
case 'creditGroup':
item.componentProps.options = options.value.creditGroup;
break;
}
}
}
await loadOptions();

View File

@@ -0,0 +1,101 @@
<template>
<div>
<n-card :bordered="false" class="proCard">
<div class="n-layout-page-header">
<n-card :bordered="false" title="交易退款">
<!-- 这是系统自动生成的CURD表格你可以将此行注释改为表格的描述 -->
</n-card>
</div>
<BasicForm
@register="register"
@submit="reloadTable"
@reset="reloadTable"
@keyup.enter="reloadTable"
ref="searchFormRef"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:openChecked="false"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
:resizeHeightOffset="-10000"
size="small"
>
<template #tableTitle>
<n-button
type="primary"
@click="handleExport"
class="min-left-space"
v-if="hasPermission(['/creditsLog/export'])"
>
<template #icon>
<n-icon>
<ExportOutlined />
</n-icon>
</template>
导出
</n-button>
</template>
</BasicTable>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, useForm } from '@/components/Form/index';
import { usePermission } from '@/hooks/web/usePermission';
import { List, Export } from '@/api/pay/refund';
import { columns, schemas } from './model';
import { ExportOutlined } from '@vicons/antd';
const { hasPermission } = usePermission();
const actionRef = ref();
const message = useMessage();
const searchFormRef = ref<any>({});
const actionColumn = reactive({
width: 300,
title: '操作',
key: 'action',
// fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [],
});
},
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
const loadDataTable = async (res) => {
return await List({ ...searchFormRef.value?.formModel, ...res });
};
function reloadTable() {
actionRef.value.reload();
}
function handleExport() {
message.loading('正在导出列表...', { duration: 1200 });
Export(searchFormRef.value?.formModel);
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,255 @@
import { h, ref } from 'vue';
import { NAvatar, NImage, NTag, NSwitch, NRate } from 'naive-ui';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { Dicts } from '@/api/dict/dict';
import { isNullObject } from '@/utils/is';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { getOptionLabel, getOptionTag, Options } from '@/utils/hotgo';
export interface State {
id: number;
memberId: number;
appId: string;
addonsName: string;
creditType: string;
creditGroup: string;
beforeNum: number;
num: number;
afterNum: number;
remark: string;
ip: string;
mapId: number;
status: number;
createdAt: string;
updatedAt: string;
}
export const defaultState = {
id: 0,
memberId: 0,
appId: '',
addonsName: '',
creditType: '',
creditGroup: '',
beforeNum: 0,
num: 0,
afterNum: 0,
remark: '',
ip: '',
mapId: 0,
status: 1,
createdAt: '',
updatedAt: '',
};
export function newState(state: State | null): State {
if (state !== null) {
return cloneDeep(state);
}
return cloneDeep(defaultState);
}
export const options = ref<Options>({
sys_normal_disable: [],
});
export const rules = {};
export const schemas = ref<FormSchema[]>([
{
field: 'id',
component: 'NInputNumber',
label: '变动ID',
componentProps: {
placeholder: '请输入变动ID',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'memberId',
component: 'NInputNumber',
label: '管理员ID',
componentProps: {
placeholder: '请输入管理员ID',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'appId',
component: 'NInput',
label: '应用id',
componentProps: {
placeholder: '请输入应用id',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'creditType',
component: 'NSelect',
label: '变动类型',
defaultValue: null,
componentProps: {
placeholder: '请选择变动类型',
options: [],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'creditGroup',
component: 'NSelect',
label: '变动的组别',
defaultValue: null,
componentProps: {
placeholder: '请选择变动的组别',
options: [],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'remark',
component: 'NInput',
label: '备注',
componentProps: {
placeholder: '请输入备注',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'ip',
component: 'NInput',
label: '操作人IP',
componentProps: {
placeholder: '请输入操作人IP',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
component: 'NSelect',
label: '状态',
defaultValue: null,
componentProps: {
placeholder: '请选择状态',
options: [],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'createdAt',
component: 'NDatePicker',
label: '创建时间',
componentProps: {
type: 'datetimerange',
clearable: true,
shortcuts: defRangeShortcuts(),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
]);
export const columns = [
{
title: '变动ID',
key: 'id',
},
{
title: '管理员ID',
key: 'memberId',
},
{
title: '应用id',
key: 'appId',
},
{
title: '插件名称',
key: 'addonsName',
},
{
title: '变动前',
key: 'beforeNum',
},
{
title: '变动数据',
key: 'num',
},
{
title: '变动后',
key: 'afterNum',
},
{
title: '备注',
key: 'remark',
},
{
title: '操作人IP',
key: 'ip',
},
{
title: '关联ID',
key: 'mapId',
},
{
title: '状态',
key: 'status',
render(row) {
if (isNullObject(row.status)) {
return ``;
}
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: getOptionTag(options.value.sys_normal_disable, row.status),
bordered: false,
},
{
default: () => getOptionLabel(options.value.sys_normal_disable, row.status),
}
);
},
},
{
title: '创建时间',
key: 'createdAt',
},
{
title: '修改时间',
key: 'updatedAt',
},
];
async function loadOptions() {
options.value = await Dicts({
types: ['sys_normal_disable'],
});
for (const item of schemas.value) {
switch (item.field) {
case 'status':
item.componentProps.options = options.value.sys_normal_disable;
break;
}
}
}
await loadOptions();

View File

@@ -0,0 +1,287 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="在线充值"> 余额可用于购买付费产品或商城消费 </n-card>
</div>
<n-spin :show="loading">
<n-grid class="mt-6" cols="1 s:1 m:1 l:4 xl:4 2xl:4" responsive="screen" :x-gap="12">
<n-gi span="4">
<n-card :bordered="false" class="proCard">
<n-thing>
<template #description> <span class="title">充值金额</span> </template>
<n-space>
<n-button
type="primary"
ghost
v-for="item in amounts"
:key="item"
@click="SetAmount(item)"
>
{{ item }}
<n-icon
class="check-icon"
:size="18"
:component="CheckOutlined"
v-if="amount === item && amountType === 1"
/>
</n-button>
<n-input-number v-model:value="amount" v-if="amountType === 2">
<template #prefix> </template>
</n-input-number>
</n-space>
<template #footer> <span class="title">支付方式 </span></template>
<template #action>
<n-space>
<n-button
strong
secondary
:color="item.color"
v-for="item in payTypes"
:key="item"
@click="SetPayType(item.value)"
>
<template #icon>
<n-icon :component="item.icon" />
</template>
{{ item.label }}
<n-icon
class="check-icon"
:size="18"
:component="CheckOutlined"
v-if="payType === item.value"
/>
</n-button>
</n-space>
<n-button
type="success"
class="create-order-button"
size="large"
@click="CreateOrder"
>
立即充值
</n-button>
</template>
</n-thing>
</n-card>
</n-gi>
</n-grid>
</n-spin>
<n-modal v-model:show="showQrModal" :show-icon="false" preset="dialog" :title="qrParams.name">
<n-form class="py-4">
<div class="text-center">
<qrcode-vue :value="qrParams.qrUrl" :size="220" class="canvas" style="margin: 0 auto" />
</div>
</n-form>
<template #action>
<n-space>
<n-button @click="() => (showQrModal = false)">关闭</n-button>
</n-space>
</template>
</n-modal>
<RechargeLog class="mt-6" />
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, inject } from 'vue';
import wx from 'weixin-js-sdk';
import { WechatOutlined, AlipayOutlined, QqOutlined, CheckOutlined } from '@vicons/antd';
import { useMessage } from 'naive-ui';
import { Create } from '@/api/order';
import QrcodeVue from 'qrcode.vue';
import RechargeLog from '../rechargeLog/index.vue';
import { SocketEnum } from '@/enums/socketEnum';
import { addOnMessage } from '@/utils/websocket';
const showQrModal = ref(false);
const qrParams = ref({
name: '',
qrUrl: '',
});
const loading = ref(false);
const message = useMessage();
const amountType = ref(1);
const amounts = ref([0.01, 10, 20, 30, 50, 100, '其他金额']);
const payTypes = ref([
{ value: 'wxpay', label: '微信支付', icon: WechatOutlined, color: '#18a058' },
{ value: 'alipay', label: '支付宝', icon: AlipayOutlined, color: '#2d8cf0' },
{ value: 'qqpay', label: 'QQ支付', icon: QqOutlined, color: '#2d8cf0' },
]);
const amount = ref<any>(null);
const payType = ref<any>(null);
onMounted(() => {});
function SetPayType(type: string) {
payType.value = type;
}
function SetAmount(a: number | string) {
amount.value = a;
if (a === '其他金额') {
amountType.value = 2;
amount.value = null;
} else {
amountType.value = 1;
}
}
function CreateOrder() {
if (amount.value === null || amount.value <= 0) {
message.error('请选择充值金额');
return;
}
if (payType.value === null || payType.value === '') {
message.error('请选择支付方式');
return;
}
loading.value = true;
Create({
orderType: 'balance',
payType: payType.value,
money: amount.value,
returnUrl: window.location.href,
})
.then((res) => {
if (res.order?.tradeType === undefined || res.order?.tradeType === '') {
message.error('创建支付订单失败,没找到交易方式,请联系管理处理!');
return;
}
if (res.order?.tradeType !== 'mp') {
if (res.order?.payURL === undefined || res.order?.payURL === '') {
message.error('创建支付订单失败,没找到支付地址,请联系管理处理!');
return;
}
}
switch (res.order?.tradeType) {
case 'scan':
showQr(res.order?.payURL, '打开微信【扫一扫】完成支付');
break;
case 'mp':
if (res.order.jsApi === undefined) {
message.error('支付失败请选择其他支付方式JSAPI支付参数无效');
return;
}
const jsApi = res.order.jsApi;
// 配置微信JS SDK
wx.config({
// debug: true,
appId: jsApi.config.app_id,
timestamp: jsApi.config.timestamp,
nonceStr: jsApi.config.nonce_str,
signature: jsApi.config.signature,
jsApiList: ['chooseWXPay'],
});
// 配置完成后返回一个resolve
wx.ready(() => {
wxJSPay({
timestamp: jsApi.params.timeStamp,
nonceStr: jsApi.params.nonceStr,
package: jsApi.params.package,
signType: jsApi.params.signType,
paySign: jsApi.params.paySign,
})
.then((_res) => {
// ...
})
.catch((err) => {
message.success('支付失败:', err.message);
});
});
break;
case 'qqweb':
showQr(res.order?.payURL, '打开QQ【扫一扫】完成支付');
break;
default:
window.open(res.order?.payURL, '_blank');
}
})
.finally(() => {
loading.value = false;
});
}
// 发起微信公众号支付
function wxJSPay(params) {
return new Promise((resolve, reject) => {
// 调用微信支付
wx.chooseWXPay({
timestamp: params.timestamp,
nonceStr: params.nonceStr,
package: params.package,
signType: params.signType,
paySign: params.paySign,
success: (res) => {
// 支付成功时返回resolve
resolve(res);
},
fail: (err) => {
// 支付失败时返回reject
reject(err);
},
});
});
}
function showQr(url: string, name: string) {
qrParams.value.qrUrl = url;
qrParams.value.name = name;
showQrModal.value = true;
}
const onMessageList = inject('onMessageList');
const handleMessageList = (res) => {
const data = JSON.parse(res.data);
if (data.event === SocketEnum.EventAdminOrderNotify) {
if (data.code == SocketEnum.CodeErr) {
message.error('查询出错:' + data.event);
return;
}
showQrModal.value = false;
message.success('支付成功');
location.reload();
return;
}
};
addOnMessage(onMessageList, handleMessageList);
</script>
<style lang="less" scoped>
::v-deep(.n-thing .n-thing-main .n-thing-main__footer:not(:first-child)) {
margin-top: 36px;
margin-bottom: 10px;
}
::v-deep(.title) {
font-weight: var(--n-title-font-weight);
transition: color 0.3s var(--n-bezier);
flex: 1;
min-width: 0;
color: var(--n-title-text-color);
font-size: 18px;
}
::v-deep(.check-icon) {
margin-left: 3px;
}
::v-deep(.create-order-button) {
margin-top: 28px;
}
</style>

View File

@@ -0,0 +1,149 @@
<template>
<div>
<n-spin :show="loading" description="请稍候...">
<n-modal
v-model:show="isShowModal"
:show-icon="false"
preset="dialog"
title="受理退款申请"
:style="{
width: dialogWidth,
}"
>
<n-form
:model="params"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-form-item label="业务单号" path="orderSn">
<n-input v-model:value="params.orderSn" :disabled="true" />
</n-form-item>
<n-form-item label="订单金额" path="money">
<n-input placeholder="请输入标题" v-model:value="params.money" :disabled="true" />
</n-form-item>
<n-form-item label="退款原因" path="refundReason">
<n-input
type="textarea"
placeholder="请填写退款原因"
v-model:value="params.refundReason"
:disabled="true"
/>
</n-form-item>
<n-form-item label="更新状态" path="status">
<n-select v-model:value="params.status" :options="options.acceptRefundStatus" />
</n-form-item>
<n-form-item label="拒绝原因" path="rejectRefundReason" v-if="params.status === 9">
<n-input
type="textarea"
placeholder="请填拒绝退款原因"
v-model:value="params.rejectRefundReason"
/>
</n-form-item>
<n-form-item label="退款备注" path="remark" v-if="params.status === 8">
<n-input type="textarea" placeholder="请填退款备注" v-model:value="params.remark" />
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="closeForm">取消</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">确定</n-button>
</n-space>
</template>
</n-modal>
</n-spin>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, computed, watch } from 'vue';
import { rules, State, newState, options } from './model';
import { useMessage } from 'naive-ui';
import { adaModalWidth } from '@/utils/hotgo';
import { AcceptRefund, View } from '@/api/order';
const emit = defineEmits(['reloadTable', 'updateShowModal']);
interface Props {
showModal: boolean;
formParams?: State;
}
const props = withDefaults(defineProps<Props>(), {
showModal: false,
formParams: () => {
return newState(null);
},
});
const isShowModal = computed({
get: () => {
return props.showModal;
},
set: (value) => {
emit('updateShowModal', value);
},
});
const loading = ref(false);
const params = ref<State>(props.formParams);
const message = useMessage();
const formRef = ref<any>({});
const dialogWidth = ref('75%');
const formBtnLoading = ref(false);
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
AcceptRefund(params.value).then((_res) => {
message.success('操作成功');
setTimeout(() => {
isShowModal.value = false;
emit('reloadTable');
});
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
onMounted(async () => {
adaModalWidth(dialogWidth);
});
function closeForm() {
isShowModal.value = false;
}
function loadForm(value) {
loading.value = true;
// 编辑
View({ id: value.id })
.then((res) => {
params.value = res;
})
.finally(() => {
loading.value = false;
});
}
watch(
() => props.formParams,
(value) => {
loadForm(value);
}
);
</script>
<style lang="less"></style>

View File

@@ -0,0 +1,132 @@
<template>
<div>
<n-spin :show="loading" description="请稍候...">
<n-modal
v-model:show="isShowModal"
:show-icon="false"
preset="dialog"
title="退款申请"
:style="{
width: dialogWidth,
}"
>
<n-form
:model="params"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-form-item label="业务单号" path="orderSn">
<n-input v-model:value="params.orderSn" :disabled="true" />
</n-form-item>
<n-form-item label="订单金额" path="money">
<n-input placeholder="请输入标题" v-model:value="params.money" :disabled="true" />
</n-form-item>
<n-form-item label="退款原因" path="refundReason">
<n-input
type="textarea"
placeholder="请填写退款原因"
v-model:value="params.refundReason"
/>
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="closeForm">取消</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">确定</n-button>
</n-space>
</template>
</n-modal>
</n-spin>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, computed, watch } from 'vue';
import { rules, State, newState } from './model';
import { useMessage } from 'naive-ui';
import { adaModalWidth } from '@/utils/hotgo';
import { ApplyRefund, View } from '@/api/order';
const emit = defineEmits(['reloadTable', 'updateShowModal']);
interface Props {
showModal: boolean;
formParams?: State;
}
const props = withDefaults(defineProps<Props>(), {
showModal: false,
formParams: () => {
return newState(null);
},
});
const isShowModal = computed({
get: () => {
return props.showModal;
},
set: (value) => {
emit('updateShowModal', value);
},
});
const loading = ref(false);
const params = ref<State>(props.formParams);
const message = useMessage();
const formRef = ref<any>({});
const dialogWidth = ref('75%');
const formBtnLoading = ref(false);
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
ApplyRefund(params.value).then((_res) => {
message.success('操作成功');
setTimeout(() => {
isShowModal.value = false;
emit('reloadTable');
});
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
onMounted(async () => {
adaModalWidth(dialogWidth);
});
function closeForm() {
isShowModal.value = false;
}
function loadForm(value) {
loading.value = true;
// 编辑
View({ id: value.id })
.then((res) => {
params.value = res;
})
.finally(() => {
loading.value = false;
});
}
watch(
() => props.formParams,
(value) => {
loadForm(value);
}
);
</script>
<style lang="less"></style>

View File

@@ -0,0 +1,45 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="充值记录" />
</div>
<n-card :bordered="false" class="proCard">
<n-tabs
type="card"
class="card-tabs"
:value="defaultTab"
animated
@before-leave="handleBeforeLeave"
>
<n-tab-pane
:name="item.key.toString()"
:tab="item.label"
v-for="item in options.status"
:key="item.key"
>
<List :type="defaultTab" />
</n-tab-pane>
</n-tabs>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import List from './list.vue';
import { useRouter } from 'vue-router';
import { options } from '@/views/asset/rechargeLog/model';
const router = useRouter();
const defaultTab = ref('-1');
onMounted(() => {
if (router.currentRoute.value.query?.type) {
defaultTab.value = router.currentRoute.value.query.type as string;
}
});
function handleBeforeLeave(tabName: string) {
defaultTab.value = tabName;
}
</script>

View File

@@ -0,0 +1,228 @@
<template>
<div>
<n-card :bordered="false" class="proCard">
<BasicForm
@register="register"
@submit="reloadTable"
@reset="reloadTable"
@keyup.enter="reloadTable"
ref="searchFormRef"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:openChecked="true"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1800"
:resizeHeightOffset="-10000"
>
<template #tableTitle>
<n-button
type="error"
@click="handleBatchDelete"
:disabled="batchDeleteDisabled"
class="min-left-space"
v-if="hasPermission(['/order/delete'])"
>
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
<n-button
type="primary"
@click="handleExport"
class="min-left-space"
v-if="hasPermission(['/order/export'])"
>
<template #icon>
<n-icon>
<ExportOutlined />
</n-icon>
</template>
导出
</n-button>
</template>
</BasicTable>
</n-card>
<ApplyRefund
@reloadTable="reloadTable"
@updateShowModal="updateShowModal"
:showModal="showModal"
:formParams="formParams"
/>
<AcceptRefund
@reloadTable="reloadTable"
@updateShowModal="updateAcceptShowModal"
:showModal="showAcceptModal"
:formParams="formParams"
/>
</div>
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, useForm } from '@/components/Form/index';
import { usePermission } from '@/hooks/web/usePermission';
import { List, Export, Delete } from '@/api/order';
import { State, columns, schemas, newState } from './model';
import { ExportOutlined, DeleteOutlined } from '@vicons/antd';
import ApplyRefund from './applyRefund.vue';
import AcceptRefund from './acceptRefund.vue';
interface Props {
type?: string;
}
const props = withDefaults(defineProps<Props>(), {
type: '-1',
});
const { hasPermission } = usePermission();
const actionRef = ref();
const dialog = useDialog();
const message = useMessage();
const searchFormRef = ref<any>({});
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
const showModal = ref(false);
const showAcceptModal = ref(false);
const formParams = ref<State>();
const actionColumn = reactive({
width: 120,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
type: 'warning',
label: '受理退款',
onClick: handleAcceptRefund.bind(null, record),
auth: ['/order/acceptRefund'],
ifShow: () => {
return record.status == 6;
},
},
{
type: 'default',
label: '申请退款',
onClick: handleApplyRefund.bind(null, record),
auth: ['/order/applyRefund'],
ifShow: () => {
return record.status == 4;
},
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
auth: ['/order/delete'],
ifShow: () => {
return record.status == 5;
},
},
],
});
},
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
const loadDataTable = async (res) => {
return await List({ ...searchFormRef.value?.formModel, ...res, ...{ status: props.type } });
};
function reloadTable() {
actionRef.value.reload();
}
function onCheckedRow(rowKeys) {
batchDeleteDisabled.value = rowKeys.length <= 0;
checkedIds.value = rowKeys;
}
function handleExport() {
message.loading('正在导出列表...', { duration: 1200 });
Export(searchFormRef.value?.formModel);
}
function handleDelete(record: Recordable) {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete(record).then((_res) => {
message.success('删除成功');
reloadTable();
});
},
onNegativeClick: () => {
// message.error('取消');
},
});
}
function handleBatchDelete() {
dialog.warning({
title: '警告',
content: '你确定要批量删除?只有已关闭的订单才能被删除',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete({ id: checkedIds.value }).then((_res) => {
message.success('删除成功');
reloadTable();
});
},
onNegativeClick: () => {
// message.error('取消');
},
});
}
function updateShowModal(value) {
showModal.value = value;
}
function handleApplyRefund(record: Recordable) {
showModal.value = true;
formParams.value = newState(record as State);
}
function updateAcceptShowModal(value) {
showAcceptModal.value = value;
}
function handleAcceptRefund(record: Recordable) {
showAcceptModal.value = true;
formParams.value = newState(record as State);
}
defineExpose({
reloadTable,
});
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,209 @@
import { h, ref } from 'vue';
import { NTag } from 'naive-ui';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { Option } from '@/api/order';
import { isNullObject } from '@/utils/is';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { getOptionLabel, getOptionTag, Options } from '@/utils/hotgo';
export interface State {
id: number;
memberId: number;
orderType: string;
productId: number;
orderSn: string;
money: number;
remark: string;
payLogOutTradeNo: string;
status: number;
createdAt: string;
updatedAt: string;
refundReason: string;
rejectRefundReason: string;
payLogPayType: string;
}
export const defaultState = {
id: 0,
memberId: 0,
orderType: '',
productId: 0,
orderSn: '',
money: 0,
payLogOutTradeNo: '',
remark: '',
status: 1,
createdAt: '',
updatedAt: '',
refundReason: '',
rejectRefundReason: '',
payLogPayType: '',
};
export function newState(state: State | null): State {
if (state !== null) {
return cloneDeep(state);
}
return cloneDeep(defaultState);
}
export const options = ref<Options>({
status: [],
acceptRefundStatus: [],
payType: [],
});
export const rules = {};
export const schemas = ref<FormSchema[]>([
{
field: 'memberId',
component: 'NInput',
label: '管理员ID',
componentProps: {
placeholder: '请输入管理员ID',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'orderSn',
component: 'NInput',
label: '业务单号',
componentProps: {
placeholder: '请输入业务订单号',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'payLogOutTradeNo',
component: 'NInput',
label: '商户单号',
componentProps: {
placeholder: '请输入商户订单号',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'createdAt',
component: 'NDatePicker',
label: '创建时间',
componentProps: {
type: 'datetimerange',
clearable: true,
shortcuts: defRangeShortcuts(),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
]);
export const columns = [
{
title: '订单ID',
key: 'id',
width: 100,
},
{
title: '管理员ID',
key: 'memberId',
width: 100,
},
{
title: '业务订单号',
key: 'orderSn',
width: 260,
},
{
title: '商户订单号',
key: 'payLogOutTradeNo',
width: 260,
},
{
title: '支付方式',
key: 'payLogPayType',
render(row) {
if (isNullObject(row.payLogPayType)) {
return ``;
}
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: getOptionTag(options.value.payType, row.payLogPayType),
bordered: false,
},
{
default: () => getOptionLabel(options.value.payType, row.payLogPayType),
}
);
},
width: 150,
},
{
title: '充值金额',
key: 'money',
width: 100,
render(row) {
return '¥' + Number(row.money).toFixed(2);
},
},
{
title: '订单状态',
key: 'status',
render(row) {
if (isNullObject(row.status)) {
return ``;
}
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: getOptionTag(options.value.status, row.status),
bordered: false,
},
{
default: () =>
getOptionLabel(options.value.status, row.status) +
(row.status === 9 ? '' + row.rejectRefundReason : ''),
}
);
},
width: 150,
},
{
title: '创建时间',
key: 'createdAt',
width: 180,
},
];
async function loadOptions() {
options.value = await Option();
for (const item of schemas.value) {
switch (item.field) {
case 'status':
item.componentProps.options = options.value.status;
break;
case 'acceptRefundStatus':
item.componentProps.options = options.value.acceptRefundStatus;
break;
case 'payType':
item.componentProps.options = options.value.payType;
break;
}
}
}
await loadOptions();

View File

@@ -1,162 +0,0 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="拖拽"> 常用于卡片事项预约流程计划等 </n-card>
</div>
<n-alert title="花式拖拽演示" type="info" class="mt-4">
每个卡片都可以上下拖拽顺序另外不同卡片也可以拖拽过去拖拽过来都不在话下呢快试试O(_)O哈哈~
</n-alert>
<n-grid
cols="1 s:2 m:3 l:4 xl:4 2xl:4"
class="mt-4 proCard"
responsive="screen"
:x-gap="12"
:y-gap="8"
>
<n-grid-item>
<NCard
title="需求池"
:segmented="{ content: true, footer: true }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="info"></n-tag>
</template>
<Draggable
class="draggable-ul"
animation="300"
:list="demandList"
group="people"
itemKey="name"
>
<template #item="{ element }">
<div class="cursor-move draggable-li">
<n-tag type="info">需求</n-tag><span class="ml-2">{{ element.name }}</span>
</div>
</template>
</Draggable>
</NCard>
</n-grid-item>
<n-grid-item>
<NCard
title="开发中"
:segmented="{ content: true, footer: true }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="info"></n-tag>
</template>
<Draggable
class="draggable-ul"
animation="300"
:list="exploitList"
group="people"
itemKey="name"
>
<template #item="{ element }">
<div class="cursor-move draggable-li">
<n-tag type="warning">开发中</n-tag><span class="ml-2">{{ element.name }}</span>
</div>
</template>
</Draggable>
</NCard>
</n-grid-item>
<n-grid-item>
<NCard
title="已完成"
:segmented="{ content: true, footer: true }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="info"></n-tag>
</template>
<Draggable
class="draggable-ul"
animation="300"
:list="completeList"
group="people"
itemKey="name"
>
<template #item="{ element }">
<div class="cursor-move draggable-li">
<n-tag type="error">已完成</n-tag><span class="ml-2">{{ element.name }}</span>
</div>
</template>
</Draggable>
</NCard>
</n-grid-item>
<n-grid-item>
<NCard
title="已验收"
:segmented="{ content: true, footer: true }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="info"></n-tag>
</template>
<Draggable
class="draggable-ul"
animation="300"
:list="approvedList"
group="people"
itemKey="name"
>
<template #item="{ element }">
<div class="cursor-move draggable-li">
<n-tag type="success">已验收</n-tag><span class="ml-2">{{ element.name }}</span>
</div>
</template>
</Draggable>
</NCard>
</n-grid-item>
</n-grid>
</div>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import Draggable from 'vuedraggable';
const demandList = reactive([
{ name: '预约表单页面,能填写预约相关信息', id: 1 },
{ name: '促销活动页面,包含促销广告展示', id: 2 },
{ name: '商品列表,需要一个到货提醒功能', id: 3 },
{ name: '商品需要一个评价功能', id: 4 },
{ name: '商品图片需要提供放大镜', id: 5 },
{ name: '订单需要提供删除到回收站', id: 6 },
{ name: '用户头像上传,需要支持裁剪', id: 7 },
{ name: '据说Vue3.2发布了setup啥时候支持', id: 8 },
]);
const exploitList = reactive([{ name: '商品图片需要提供放大镜', id: 5 }]);
const completeList = reactive([{ name: '商品图片需要提供放大镜', id: 5 }]);
const approvedList = reactive([{ name: '商品图片需要提供放大镜', id: 5 }]);
</script>
<style lang="less" scoped>
.draggable-ul {
width: 100%;
overflow: hidden;
margin-top: -16px;
.draggable-li {
width: 100%;
padding: 16px 10px;
color: #333;
border-bottom: 1px solid #efeff5;
}
}
</style>

View File

@@ -1,172 +0,0 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="基础表单"> 基础表单用于向用户收集表单信息 </n-card>
</div>
<n-card :bordered="false" class="mt-4 proCard">
<div class="BasicForm">
<BasicForm
submitButtonText="提交预约"
layout="horizontal"
:gridProps="{ cols: 1 }"
:schemas="schemas"
@submit="handleSubmit"
@reset="handleReset"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
</div>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { BasicForm } from '@/components/Form/index';
import { useMessage } from 'naive-ui';
const schemas = [
{
field: 'name',
component: 'NInput',
label: '姓名',
labelMessage: '这是一个提示',
componentProps: {
placeholder: '请输入姓名',
onInput: (e: any) => {
console.log(e);
},
},
rules: [{ required: true, message: '请输入姓名', trigger: ['blur'] }],
},
{
field: 'mobile',
component: 'NInputNumber',
label: '手机',
componentProps: {
placeholder: '请输入手机号码',
showButton: false,
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'type',
component: 'NSelect',
label: '类型',
componentProps: {
placeholder: '请选择类型',
options: [
{
label: '舒适性',
value: 1,
},
{
label: '经济性',
value: 2,
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeDate',
component: 'NDatePicker',
label: '预约时间',
defaultValue: 1183135260000,
componentProps: {
type: 'date',
clearable: true,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeTime',
component: 'NTimePicker',
label: '停留时间',
componentProps: {
clearable: true,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeProject',
component: 'NCheckbox',
label: '预约项目',
componentProps: {
placeholder: '请选择预约项目',
options: [
{
label: '种牙',
value: 1,
},
{
label: '补牙',
value: 2,
},
{
label: '根管',
value: 3,
},
],
onUpdateChecked: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeSource',
component: 'NRadioGroup',
label: '来源',
componentProps: {
options: [
{
label: '网上',
value: 1,
},
{
label: '门店',
value: 2,
},
],
onUpdateChecked: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
label: '状态',
//插槽
slot: 'statusSlot',
},
];
const message = useMessage();
function handleSubmit(values: Recordable) {
console.log(values);
message.success(JSON.stringify(values));
}
function handleReset(values: Recordable) {
console.log(values);
}
</script>
<style lang="less" scoped>
.BasicForm {
width: 550px;
margin: 0 auto;
overflow: hidden;
padding-top: 20px;
}
</style>

View File

@@ -1,195 +0,0 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="基础表单"> useForm 表单用于向用户收集表单信息 </n-card>
</div>
<n-card :bordered="false" class="mt-4 proCard">
<div class="BasicForm">
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset">
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
</div>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { useMessage } from 'naive-ui';
const schemas: FormSchema[] = [
{
field: 'name',
component: 'NInput',
label: '姓名',
labelMessage: '这是一个提示',
giProps: {
span: 1,
},
componentProps: {
placeholder: '请输入姓名',
onInput: (e: any) => {
console.log(e);
},
},
rules: [{ required: true, message: '请输入姓名', trigger: ['blur'] }],
},
{
field: 'mobile',
component: 'NInputNumber',
label: '手机',
componentProps: {
placeholder: '请输入手机号码',
showButton: false,
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'type',
component: 'NSelect',
label: '类型',
giProps: {
//span: 24,
},
componentProps: {
placeholder: '请选择类型',
options: [
{
label: '舒适性',
value: 1,
},
{
label: '经济性',
value: 2,
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeDate',
component: 'NDatePicker',
label: '预约时间',
giProps: {
//span: 24,
},
defaultValue: 1183135260000,
componentProps: {
type: 'date',
clearable: true,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeTime',
component: 'NTimePicker',
label: '停留时间',
giProps: {
//span: 24,
},
componentProps: {
clearable: true,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeProject',
component: 'NCheckbox',
label: '预约项目',
giProps: {
//span: 24,
},
componentProps: {
placeholder: '请选择预约项目',
options: [
{
label: '种牙',
value: 1,
},
{
label: '补牙',
value: 2,
},
{
label: '根管',
value: 3,
},
],
onUpdateChecked: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeSource',
component: 'NRadioGroup',
label: '来源',
giProps: {
//span: 24,
},
componentProps: {
options: [
{
label: '网上',
value: 1,
},
{
label: '门店',
value: 2,
},
],
onUpdateChecked: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
label: '状态',
giProps: {
//span: 24,
},
//插槽
slot: 'statusSlot',
},
];
const message = useMessage();
const [register, {}] = useForm({
gridProps: { cols: 1 },
collapsedRows: 3,
labelWidth: 120,
layout: 'horizontal',
submitButtonText: '提交预约',
schemas,
});
function handleSubmit(values: Recordable) {
console.log(values);
message.success(JSON.stringify(values));
}
function handleReset(values: Recordable) {
console.log(values);
}
</script>
<style lang="less" scoped>
.BasicForm {
width: 550px;
margin: 0 auto;
overflow: hidden;
padding-top: 20px;
}
</style>

View File

@@ -1,306 +0,0 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="模态框">
模态框用于向用户收集或展示信息Modal 采用 Dialog 预设扩展拖拽效果
<br />
以下是 useModal
方式ref方式也支持使用方式和其他组件一致modalRef.value.closeModal()
</n-card>
</div>
<n-card :bordered="false" class="proCard mt-4">
<n-alert title="Modal嵌套Form" type="info">
使用 useModal 进行弹窗展示和操作并演示了在Modal内和Form组件组合使用方法
</n-alert>
<n-divider />
<n-space>
<n-button type="primary" @click="showModal">打开Modal嵌套Form例子</n-button>
</n-space>
<n-divider />
<n-alert title="个性化轻量级" type="info">
使用 useModal 进行弹窗展示和操作自定义配置实现轻量级效果更多配置请参考文档
</n-alert>
<n-divider />
<n-space>
<n-button type="primary" @click="showLightModal">轻量级确认</n-button>
</n-space>
<n-divider />
<n-alert title="提示" type="info">
组件暴露了setProps 方法用于修改组件内部
Props比如标题具体参考UI框架文档DialogReactive Properties
</n-alert>
</n-card>
<basicModal @register="modalRegister" ref="modalRef" class="basicModal" @on-ok="okModal">
<template #default>
<BasicForm @register="register" @reset="handleReset" class="basicForm">
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
</template>
</basicModal>
<basicModal
@register="lightModalRegister"
class="basicModalLight"
ref="modalRef"
@on-ok="lightOkModal"
>
<template #default>
<p class="text-gray-500" style="padding-left: 35px">一些对话框内容</p>
</template>
</basicModal>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, reactive, toRefs } from 'vue';
import { useMessage } from 'naive-ui';
import { basicModal, useModal } from '@/components/Modal';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
const schemas: FormSchema[] = [
{
field: 'name',
component: 'NInput',
label: '姓名',
labelMessage: '这是一个提示',
giProps: {
span: 1,
},
componentProps: {
placeholder: '请输入姓名',
onInput: (e: any) => {
console.log(e);
},
},
rules: [{ required: true, message: '请输入姓名', trigger: ['blur'] }],
},
{
field: 'mobile',
component: 'NInputNumber',
label: '手机',
componentProps: {
placeholder: '请输入手机号码',
showButton: false,
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'type',
component: 'NSelect',
label: '类型',
giProps: {
//span: 24,
},
componentProps: {
placeholder: '请选择类型',
options: [
{
label: '舒适性',
value: 1,
},
{
label: '经济性',
value: 2,
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeDate',
component: 'NDatePicker',
label: '预约时间',
giProps: {
//span: 24,
},
defaultValue: 1183135260000,
componentProps: {
type: 'date',
clearable: true,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeTime',
component: 'NTimePicker',
label: '停留时间',
giProps: {
//span: 24,
},
componentProps: {
clearable: true,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeProject',
component: 'NCheckbox',
label: '预约项目',
giProps: {
//span: 24,
},
componentProps: {
placeholder: '请选择预约项目',
options: [
{
label: '种牙',
value: 1,
},
{
label: '补牙',
value: 2,
},
{
label: '根管',
value: 3,
},
],
onUpdateChecked: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeSource',
component: 'NRadioGroup',
label: '来源',
giProps: {
//span: 24,
},
componentProps: {
options: [
{
label: '网上',
value: 1,
},
{
label: '门店',
value: 2,
},
],
onUpdateChecked: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
label: '状态',
giProps: {
//span: 24,
},
//插槽
slot: 'statusSlot',
},
];
export default defineComponent({
components: { basicModal, BasicForm },
setup() {
const modalRef: any = ref(null);
const message = useMessage();
const [modalRegister, { openModal, closeModal, setSubLoading }] = useModal({
title: '新增预约',
});
const [
lightModalRegister,
{
openModal: lightOpenModal,
closeModal: lightCloseModal,
setSubLoading: lightSetSubLoading,
},
] = useModal({
title: '确认对话框',
showIcon: true,
type: 'warning',
closable: false,
maskClosable: true,
});
const [register, { submit }] = useForm({
gridProps: { cols: 1 },
collapsedRows: 3,
labelWidth: 120,
layout: 'horizontal',
submitButtonText: '提交预约',
showActionButtonGroup: false,
schemas,
});
const state = reactive({
formValue: {
name: '小马哥',
},
});
async function okModal() {
const formRes = await submit();
if (formRes) {
closeModal();
message.success('提交成功');
} else {
message.error('验证失败,请填写完整信息');
setSubLoading(false);
}
}
function lightOkModal() {
lightCloseModal();
lightSetSubLoading(false);
}
function showLightModal() {
lightOpenModal();
}
function showModal() {
openModal();
}
function handleReset(values: Recordable) {
console.log(values);
}
return {
...toRefs(state),
modalRef,
register,
modalRegister,
lightModalRegister,
handleReset,
showModal,
okModal,
lightOkModal,
showLightModal,
};
},
});
</script>
<style lang="less">
.basicForm {
padding-top: 20px;
}
.n-dialog.basicModal {
width: 640px;
}
.n-dialog.basicModalLight {
width: 416px;
padding-top: 26px;
}
</style>

View File

@@ -1,114 +0,0 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="富文本">
富文本用于展示图文信息比如商品详情文章详情等...
</n-card>
</div>
<n-card :bordered="false" class="mt-4 proCard">
<QuillEditor
ref="quillEditor"
:options="options"
v-model:content="myContent"
style="height: 350px"
@ready="readyQuill"
class="quillEditor"
/>
<template #footer>
<n-space>
<n-button @click="addText">增加文本</n-button>
<n-button @click="addImg">增加图片</n-button>
<n-button @click="getHtml">获取HTML</n-button>
</n-space>
</template>
</n-card>
<n-card :bordered="false" class="mt-4 proCard" title="HTML 内容">
<n-input
v-model:value="myContentHtml"
type="textarea"
placeholder="html"
:autosize="{
minRows: 3,
maxRows: 6,
}"
/>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import { QuillEditor } from '@vueup/vue-quill';
import '@vueup/vue-quill/dist/vue-quill.snow.css';
const quillEditor = ref();
const myContent = ref(
'<h4>Naive Ui Admin 是一个基于 vue3,vite2,TypeScript 的中后台解决方案</h4>'
);
const myContentHtml = ref(
'<h4>Naive Ui Admin 是一个基于 vue3,vite2,TypeScript 的中后台解决方案</h4>'
);
const options = reactive({
modules: {
toolbar: [
['bold', 'italic', 'underline', 'strike'], // toggled buttons
['blockquote', 'code-block'],
[{ header: 1 }, { header: 2 }], // custom button values
[{ list: 'ordered' }, { list: 'bullet' }],
[{ script: 'sub' }, { script: 'super' }], // superscript/subscript
[{ indent: '-1' }, { indent: '+1' }], // outdent/indent
[{ direction: 'rtl' }], // text direction
[{ size: ['small', false, 'large', 'huge'] }], // custom dropdown
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ color: [] }, { background: [] }], // dropdown with defaults from theme
[{ font: [] }],
[{ align: [] }],
['clean'],
['image'],
],
},
theme: 'snow',
placeholder: '输入您喜欢的内容吧!',
});
function readyQuill() {
console.log('Quill准备好了');
}
function getHtml() {
myContentHtml.value = getHtmlVal();
}
function addText() {
const html = getHtmlVal() + '新增加的内容';
quillEditor.value.setHTML(html);
}
function addImg() {
const html =
getHtmlVal() +
'<img style="width:100px" src="https://www.baidu.com/img/flexible/logo/pc/result.png"/>';
quillEditor.value.setHTML(html);
}
function getHtmlVal() {
return quillEditor.value.getHTML();
}
</script>
<style lang="less">
.ql-toolbar.ql-snow {
border-top: none;
border-left: none;
border-right: none;
border-bottom: 1px solid #eee;
margin-top: -10px;
}
.ql-container.ql-snow {
border: none;
}
</style>

View File

@@ -1,83 +0,0 @@
import { h } from 'vue';
import { NAvatar } from 'naive-ui';
export const columns = [
{
title: 'id',
key: 'id',
width: 100,
},
{
title: '编码',
key: 'no',
width: 100,
},
{
title: '名称',
key: 'name',
editComponent: 'NInput',
// 默认必填校验
editRule: true,
edit: true,
width: 200,
},
{
title: '头像',
key: 'avatar',
width: 100,
render(row) {
return h(NAvatar, {
size: 48,
src: row.avatar,
});
},
},
{
title: '地址',
key: 'address',
editComponent: 'NSelect',
editComponentProps: {
options: [
{
label: '广东省',
value: 1,
},
{
label: '浙江省',
value: 2,
},
],
},
edit: true,
width: 200,
ellipsis: false,
},
{
title: '开始日期',
key: 'beginTime',
edit: true,
width: 160,
editComponent: 'NDatePicker',
editComponentProps: {
type: 'datetime',
format: 'yyyy-MM-dd HH:mm:ss',
valueFormat: 'yyyy-MM-dd HH:mm:ss',
},
ellipsis: false,
},
{
title: '结束日期',
key: 'endTime',
width: 160,
},
{
title: '创建时间',
key: 'date',
width: 160,
},
{
title: '停留时间',
key: 'time',
width: 80,
},
];

View File

@@ -1,113 +0,0 @@
<template>
<n-card :bordered="false" class="proCard">
<BasicTable
title="表格列表"
titleTooltip="这是一个提示"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
:scroll-x="1360"
@update:checked-row-keys="onCheckedRow"
>
<template #toolbar>
<n-button type="primary" @click="reloadTable">刷新数据</n-button>
</template>
</BasicTable>
</n-card>
</template>
<script lang="ts" setup>
import { reactive, ref, h } from 'vue';
import { BasicTable, TableAction } from '@/components/Table';
import { getTableList } from '@/api/table/list';
import { columns } from './basicColumns';
import { useDialog, useMessage } from 'naive-ui';
import { DeleteOutlined, EditOutlined } from '@vicons/antd';
const message = useMessage();
const dialog = useDialog();
const actionRef = ref();
const params = reactive({
pageSize: 5,
name: 'xiaoMa',
});
const actionColumn = reactive({
width: 150,
title: '操作',
key: 'action',
fixed: 'right',
align: 'center',
render(record) {
return h(TableAction as any, {
style: 'text',
actions: createActions(record),
});
},
});
function createActions(record) {
return [
{
label: '删除',
type: 'error',
// 配置 color 会覆盖 type
color: 'red',
icon: DeleteOutlined,
onClick: handleDelete.bind(null, record),
// 根据业务控制是否显示 isShow 和 auth 是并且关系
ifShow: () => {
return true;
},
// 根据权限控制是否显示: 有权限,会显示,支持多个
auth: ['basic_list'],
},
{
label: '编辑',
type: 'primary',
icon: EditOutlined,
onClick: handleEdit.bind(null, record),
ifShow: () => {
return true;
},
auth: ['basic_list'],
},
];
}
const loadDataTable = async (res) => {
return await getTableList({ ...params, ...res });
};
function onCheckedRow(rowKeys) {
console.log(rowKeys);
}
function reloadTable() {
actionRef.value.reload();
}
function handleDelete(record) {
console.log(record);
dialog.info({
title: '提示',
content: `您想删除${record.name}`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
message.success('删除成功');
},
onNegativeClick: () => {},
});
}
function handleEdit(record) {
console.log(record);
message.success('您点击了编辑按钮');
}
</script>
<style lang="less" scoped></style>

View File

@@ -1,72 +0,0 @@
import { h } from 'vue';
import { NAvatar, NTag } from 'naive-ui';
export const columns = [
{
title: 'id',
key: 'id',
width: 100,
},
{
title: '编码',
key: 'no',
width: 100,
},
{
title: '名称',
key: 'name',
width: 100,
},
{
title: '头像',
key: 'avatar',
width: 100,
render(row) {
return h(NAvatar, {
size: 48,
src: row.avatar,
});
},
},
{
title: '地址',
key: 'address',
width: 150,
},
{
title: '开始日期',
key: 'beginTime',
width: 160,
},
{
title: '结束日期',
key: 'endTime',
width: 160,
},
{
title: '状态',
key: 'status',
width: 100,
render(row) {
return h(
NTag,
{
type: row.status ? 'success' : 'error',
},
{
default: () => (row.status ? '启用' : '禁用'),
}
);
},
},
{
title: '创建时间',
key: 'date',
width: 160,
},
{
title: '停留时间',
key: 'time',
width: 80,
},
];

View File

@@ -1,59 +0,0 @@
<template>
<n-card :bordered="false" class="proCard">
<BasicTable
title="表格列表"
titleTooltip="这是一个提示"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
@edit-end="editEnd"
@edit-change="onEditChange"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1360"
>
<template #toolbar>
<n-button type="primary" @click="reloadTable">刷新数据</n-button>
</template>
</BasicTable>
</n-card>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { BasicTable } from '@/components/Table';
import { getTableList } from '@/api/table/list';
import { columns } from './CellColumns';
const actionRef = ref();
const params = reactive({
pageSize: 5,
name: 'xiaoMa',
});
function onEditChange({ column, value, record }) {
if (column.key === 'id') {
record.editValueRefs.name4.value = `${value}`;
}
console.log(column, value, record);
}
const loadDataTable = async (res) => {
return await getTableList({ ...params, ...res });
};
function onCheckedRow(rowKeys) {
console.log(rowKeys);
}
function reloadTable() {
console.log(actionRef.value);
actionRef.value.reload();
}
function editEnd({ record, index, key, value }) {
console.log(value);
}
</script>
<style lang="less" scoped></style>

View File

@@ -1,114 +0,0 @@
<template>
<n-card :bordered="false" class="proCard">
<BasicTable
title="表格列表"
titleTooltip="这是一个提示"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@edit-end="editEnd"
@edit-change="onEditChange"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1590"
>
<template #toolbar>
<n-button type="primary" @click="reloadTable">刷新数据</n-button>
</template>
</BasicTable>
</n-card>
</template>
<script lang="ts" setup>
import { reactive, ref, h } from 'vue';
import { BasicTable, TableAction } from '@/components/Table';
import { getTableList } from '@/api/table/list';
import { columns } from './rowColumns';
const actionRef = ref();
const currentEditKeyRef = ref('');
const params = reactive({
pageSize: 5,
name: 'xiaoMa',
});
const actionColumn = reactive({
width: 150,
title: '操作',
key: 'action',
fixed: 'right',
align: 'center',
render(record) {
return h(TableAction, {
style: 'button',
actions: createActions(record),
});
},
});
function handleEdit(record) {
currentEditKeyRef.value = record.key;
record.onEdit?.(true);
}
function handleCancel(record) {
currentEditKeyRef.value = '';
record.onEdit?.(false, false);
}
function onEditChange({ column, value, record }) {
if (column.key === 'id') {
record.editValueRefs.name4.value = `${value}`;
}
console.log(column, value, record);
}
async function handleSave(record) {
const pass = await record.onEdit?.(false, true);
if (pass) {
currentEditKeyRef.value = '';
}
}
function createActions(record) {
if (!record.editable) {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
];
} else {
return [
{
label: '保存',
onClick: handleSave.bind(null, record),
},
{
label: '取消',
onClick: handleCancel.bind(null, record),
},
];
}
}
const loadDataTable = async (res) => {
return await getTableList({ ...params, ...res });
};
function onCheckedRow(rowKeys) {
console.log(rowKeys);
}
function reloadTable() {
console.log(actionRef.value);
actionRef.value.reload();
}
function editEnd({ record, index, key, value }) {
console.log(value);
}
</script>
<style lang="less" scoped></style>

View File

@@ -1,97 +0,0 @@
import { h } from 'vue';
import { NAvatar } from 'naive-ui';
export const columns = [
{
title: 'id',
key: 'id',
width: 100,
},
{
title: '编码',
key: 'no',
width: 100,
},
{
title: '名称',
key: 'name',
editComponent: 'NInput',
editRow: true,
// 默认必填校验
editRule: true,
edit: true,
width: 200,
},
{
title: '头像',
key: 'avatar',
width: 100,
render(row) {
return h(NAvatar, {
size: 48,
src: row.avatar,
});
},
},
{
title: '地址',
key: 'address',
editRow: true,
editComponent: 'NSelect',
editComponentProps: {
options: [
{
label: '广东省',
value: 1,
},
{
label: '浙江省',
value: 2,
},
],
},
edit: true,
width: 200,
ellipsis: false,
},
{
title: '开始日期',
key: 'beginTime',
editRow: true,
edit: true,
width: 240,
editComponent: 'NDatePicker',
editComponentProps: {
type: 'datetime',
format: 'yyyy-MM-dd HH:mm:ss',
valueFormat: 'yyyy-MM-dd HH:mm:ss',
},
ellipsis: false,
},
{
title: '结束日期',
key: 'endTime',
width: 160,
},
{
title: '状态',
key: 'status',
editRow: true,
edit: true,
width: 100,
editComponent: 'NSwitch',
editValueMap: (value) => {
return value ? '启用' : '禁用';
},
},
{
title: '创建时间',
key: 'date',
width: 160,
},
{
title: '停留时间',
key: 'time',
width: 80,
},
];

View File

@@ -1,111 +0,0 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="上传图片"> 上传图片用于向用户收集图片信息 </n-card>
</div>
<n-card :bordered="false" class="mt-4 proCard">
<n-grid cols="2 s:1 m:3 l:3 xl:3 2xl:3" responsive="screen">
<n-grid-item offset="0 s:0 m:1 l:1 xl:1 2xl:1">
<n-form
:label-width="80"
:model="formValue"
:rules="rules"
label-placement="left"
ref="formRef"
class="py-8"
>
<n-form-item label="预约姓名" path="name">
<n-input v-model:value="formValue.name" placeholder="输入姓名" />
</n-form-item>
<n-form-item label="预约号码" path="mobile">
<n-input placeholder="电话号码" v-model:value="formValue.mobile" />
</n-form-item>
<n-form-item label="病例图片" path="images">
<BasicUpload
:action="`${uploadUrl}/v1.0/files`"
:headers="uploadHeaders"
:data="{ type: 0 }"
name="files"
:width="100"
:height="100"
@uploadChange="uploadChange"
v-model:value="formValue.images"
helpText="单个文件不超过2MB最多只能上传10个文件"
/>
</n-form-item>
<div style="margin-left: 80px">
<n-space>
<n-button type="primary" @click="formSubmit">提交预约</n-button>
<n-button @click="resetForm">重置</n-button>
</n-space>
</div>
</n-form>
</n-grid-item>
</n-grid>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { ref, unref, reactive } from 'vue';
import { useMessage } from 'naive-ui';
import { BasicUpload } from '@/components/Upload';
import { useGlobSetting } from '@/hooks/setting';
const globSetting = useGlobSetting();
const rules = {
name: {
required: true,
message: '请输入预约姓名',
trigger: 'blur',
},
remark: {
required: true,
message: '请输入预约备注',
trigger: 'blur',
},
images: {
required: true,
type: 'array',
message: '请上传病例图片',
trigger: 'change',
},
};
const formRef: any = ref(null);
const message = useMessage();
const { uploadUrl } = globSetting;
const formValue = reactive({
name: '',
mobile: '',
//图片列表 通常查看和编辑使用 绝对路径 | 相对路径都可以
images: ['https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'],
});
const uploadHeaders = reactive({
platform: 'miniPrograms',
timestamp: new Date().getTime(),
token: 'Q6fFCuhc1vkKn5JNFWaCLf6gRAc5n0LQHd08dSnG4qo=',
});
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
message.success('验证成功');
} else {
message.error('验证失败,请填写完整信息');
}
});
}
function resetForm() {
formRef.value.restoreValidation();
}
function uploadChange(list: string[]) {
formValue.images = unref(list);
}
</script>

View File

@@ -60,14 +60,12 @@
</n-descriptions-item>
<n-descriptions-item label="状态">
<template v-for="(item, key) in formValue?.status" :key="key">
<n-tag
:type="getOptionTag(options.sys_normal_disable, item)"
size="small"
class="min-left-space"
>{{ getOptionLabel(options.sys_normal_disable, item) }}</n-tag
>
</template>
<n-tag
:type="getOptionTag(options.sys_normal_disable, formValue?.status)"
size="small"
class="min-left-space"
>{{ getOptionLabel(options.sys_normal_disable, formValue?.status) }}</n-tag
>
</n-descriptions-item>

View File

@@ -1,7 +0,0 @@
<template>
<div>监控台</div>
</template>
<script lang="ts" setup></script>
<style lang="less" scoped></style>

View File

@@ -120,7 +120,6 @@
NIcon,
NTag,
NIconWrapper,
NAvatar,
useMessage,
NImage,
useDialog,
@@ -160,13 +159,25 @@
});
} else {
return h(
NAvatar,
NIconWrapper,
{
size: 48,
color: '#2D8CF0',
borderRadius: 8,
},
{
default: () => h(getIconComponent(row.logo)),
default: () =>
h(
NIcon,
{
size: 36,
style: {
marginTop: '-8px',
},
},
{
default: () => h(getIconComponent(row.logo)),
}
),
}
);
}

View File

@@ -29,6 +29,7 @@
HelpCircleOutline,
RemoveCircleOutline,
} from '@vicons/ionicons5';
hljs.registerLanguage('goLang', goLang);
interface Props {

View File

@@ -1,194 +0,0 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="基础表单">
表单页用于向用户收集或验证信息基础表单常见于数据项较少的表单场景表单域标签也可支持响应式
</n-card>
</div>
<n-card :bordered="false" class="mt-4 proCard">
<n-grid cols="1 s:1 m:3 l:3 xl:3 2xl:3" responsive="screen">
<n-grid-item offset="0 s:0 m:1 l:1 xl:1 2xl:1">
<n-form
:label-width="80"
:model="formValue"
:rules="rules"
label-placement="left"
ref="formRef"
class="py-8"
>
<n-form-item label="预约姓名1" path="name">
<n-input v-model:value="formValue.name" placeholder="输入姓名" />
</n-form-item>
<n-form-item label="预约号码" path="mobile">
<n-input placeholder="电话号码" v-model:value="formValue.mobile" />
</n-form-item>
<n-form-item label="预约时间" path="datetime">
<n-date-picker type="datetime" v-model:value="formValue.datetime" />
</n-form-item>
<n-form-item label="预约医生" path="doctor">
<n-select
placeholder="请选择预约医生"
:options="doctorList"
v-model:value="formValue.doctor"
/>
</n-form-item>
<n-form-item label="预约事项" path="matter">
<n-select
placeholder="请选择预约事项"
:options="matterList"
v-model:value="formValue.matter"
multiple
/>
</n-form-item>
<n-form-item label="性别" path="sex">
<n-radio-group v-model:value="formValue.sex" name="sex">
<n-space>
<n-radio :value="1"></n-radio>
<n-radio :value="2"></n-radio>
</n-space>
</n-radio-group>
</n-form-item>
<n-form-item label="预约备注" path="remark">
<n-input
v-model:value="formValue.remark"
type="textarea"
placeholder="请输入预约备注"
/>
</n-form-item>
<n-form-item label="图片" path="img">
<BasicUpload
:action="`${uploadUrl}/v1.0/files`"
:headers="uploadHeaders"
:data="{ type: 0 }"
name="files"
:width="100"
:height="100"
@uploadChange="uploadChange"
v-model:value="uploadList"
helpText="单个文件不超过20MB最多只能上传10个文件"
/>
</n-form-item>
<div style="margin-left: 80px">
<n-space>
<n-button type="primary" @click="formSubmit">提交预约</n-button>
<n-button @click="resetForm">重置</n-button>
</n-space>
</div>
</n-form>
</n-grid-item>
</n-grid>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { ref, unref, reactive } from 'vue';
import { useMessage } from 'naive-ui';
import { BasicUpload } from '@/components/Upload';
import { useGlobSetting } from '@/hooks/setting';
const globSetting = useGlobSetting();
const matterList = [
{
label: '种牙',
value: 1,
},
{
label: '补牙',
value: 2,
},
{
label: '根管',
value: 3,
},
];
const doctorList = [
{
label: '李医生',
value: 1,
},
{
label: '黄医生',
value: 2,
},
{
label: '张医生',
value: 3,
},
];
const rules = {
name: {
required: true,
message: '请输入预约姓名',
trigger: 'blur',
},
remark: {
required: true,
message: '请输入预约备注',
trigger: 'blur',
},
mobile: {
required: true,
message: '请输入预约电话号码',
trigger: ['input'],
},
datetime: {
required: true,
type: 'number',
message: '请选择预约时间',
trigger: ['blur', 'change'],
},
doctor: {
required: true,
type: 'number',
message: '请选择预约医生',
trigger: 'change',
},
};
const formRef: any = ref(null);
const message = useMessage();
const { uploadUrl } = globSetting;
const defaultValueRef = () => ({
name: '',
mobile: '',
remark: '',
sex: 1,
matter: null,
doctor: null,
datetime: [],
});
let formValue = reactive(defaultValueRef());
const uploadList = ref([
'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
]);
const uploadHeaders = reactive({
platform: 'miniPrograms',
timestamp: new Date().getTime(),
token: 'Q6fFCuhc1vkKn5JNFWaCLf6gRAc5n0LQHd08dSnG4qo=',
});
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
message.success('验证成功');
} else {
message.error('验证失败,请填写完整信息');
}
});
}
function resetForm() {
formRef.value.restoreValidation();
formValue = Object.assign(unref(formValue), defaultValueRef());
}
function uploadChange(list: string[]) {
console.log(list);
}
</script>

View File

@@ -1,124 +0,0 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="表单详情">
表单除了提交数据有时也用于显示只读信息
</n-card>
</div>
<n-card
:bordered="false"
title="基本信息"
class="mt-4 proCard"
size="small"
:segmented="{ content: true }"
>
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item>
<template #label>收款人姓名</template>
啊俊
</n-descriptions-item>
<n-descriptions-item label="收款账户">NaiveUiAdmin@qq.com</n-descriptions-item>
<n-descriptions-item label="付款类型">支付宝</n-descriptions-item>
<n-descriptions-item label="付款账户">NaiveUiAdmin@163.com</n-descriptions-item>
<n-descriptions-item label="转账金额">1980.00</n-descriptions-item>
<n-descriptions-item label="状态">
<n-tag type="success"> 已到账</n-tag>
</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
title="其它信息"
class="mt-4 proCard"
size="small"
:segmented="{ content: true }"
>
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item>
<template #label>城市</template>
深圳
</n-descriptions-item>
<n-descriptions-item label="性别"></n-descriptions-item>
<n-descriptions-item label="邮箱">NaiveUiAdmin@qq.com</n-descriptions-item>
<n-descriptions-item label="地址">广东省深圳市南山区</n-descriptions-item>
<n-descriptions-item label="生日">1991-06-04</n-descriptions-item>
<n-descriptions-item label="认证">
<n-tag type="success"> 已认证</n-tag>
</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
title="表格信息"
class="mt-4 proCard"
size="small"
:segmented="{ content: true }"
>
<n-table :bordered="false" :single-line="false">
<thead>
<tr>
<th>姓名</th>
<th>性别</th>
<th>城市</th>
<th>生日</th>
<th width="150">操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>Ah jung</td>
<td></td>
<td>深圳</td>
<td>1993-11-09</td>
<td>
<n-space>
<n-button size="small" type="error">删除</n-button>
<n-button size="small" type="info">查看</n-button>
</n-space>
</td>
</tr>
<tr>
<td>西门飞雪</td>
<td></td>
<td>广州</td>
<td>1991-09-11</td>
<td>
<n-space>
<n-button size="small" type="error">删除</n-button>
<n-button size="small" type="info">查看</n-button>
</n-space>
</td>
</tr>
<tr>
<td>泰坦巨人</td>
<td></td>
<td>北京</td>
<td>1990-11-03</td>
<td>
<n-space>
<n-button size="small" type="error">删除</n-button>
<n-button size="small" type="info">查看</n-button>
</n-space>
</td>
</tr>
<tr>
<td>猎魔人</td>
<td></td>
<td>上海</td>
<td>1992-03-11</td>
<td>
<n-space>
<n-button size="small" type="error">删除</n-button>
<n-button size="small" type="info">查看</n-button>
</n-space>
</td>
</tr>
</tbody>
</n-table>
</n-card>
</div>
</template>
<script setup></script>
<style lang="less" scoped></style>

View File

@@ -1,122 +0,0 @@
<template>
<n-form
:label-width="90"
:model="formValue"
:rules="rules"
label-placement="left"
ref="form1Ref"
style="max-width: 500px; margin: 40px auto 0 80px"
>
<n-form-item label="付款账户" path="myAccount">
<n-select
placeholder="请选择付款账户"
:options="myAccountList"
v-model:value="formValue.myAccount"
/>
</n-form-item>
<n-form-item label="收款账户" path="account">
<n-input-group>
<n-select
placeholder="请选择"
:options="accountTypeList"
:style="{ width: '20%' }"
v-model:value="formValue.accountType"
/>
<n-input
placeholder="请输入收款账户"
:style="{ width: '80%' }"
v-model:value="formValue.account"
/>
</n-input-group>
</n-form-item>
<n-form-item label="收款人姓名" path="name">
<n-input placeholder="请输入收款人姓名" v-model:value="formValue.name" />
</n-form-item>
<n-form-item label="转账金额" path="money">
<n-input placeholder="请输入转账金额" v-model:value="formValue.money">
<template #prefix>
<span class="text-gray-400"></span>
</template>
</n-input>
</n-form-item>
<div style="margin-left: 80px">
<n-space>
<n-button type="primary" @click="formSubmit">下一步</n-button>
</n-space>
</div>
</n-form>
</template>
<script lang="ts" setup>
import { ref, defineEmits } from 'vue';
import { useMessage } from 'naive-ui';
const myAccountList = [
{
label: 'NaiveUiAdmin@163.com',
value: 1,
},
{
label: 'NaiveUiAdmin@qq.com',
value: 2,
},
];
const accountTypeList = [
{
label: '微信',
value: 1,
},
{
label: '支付宝',
value: 2,
},
];
const emit = defineEmits(['nextStep']);
const form1Ref: any = ref(null);
const message = useMessage();
const formValue = ref({
accountType: 1,
myAccount: null,
account: 'xioama@qq.com',
money: '1980',
name: 'Ah jung',
});
const rules = {
name: {
required: true,
message: '请输入收款人姓名',
trigger: 'blur',
},
account: {
required: true,
message: '请输入收款账户',
trigger: 'blur',
},
money: {
required: true,
message: '请输入转账金额',
trigger: 'blur',
},
myAccount: {
required: true,
type: 'number',
message: '请选择付款账户',
trigger: 'change',
},
};
function formSubmit() {
form1Ref.value.validate((errors) => {
if (!errors) {
emit('nextStep');
} else {
message.error('验证失败,请填写完整信息');
}
});
}
</script>

View File

@@ -1,72 +0,0 @@
<template>
<n-form
:label-width="90"
:model="formValue"
:rules="rules"
label-placement="left"
ref="form2Ref"
style="max-width: 500px; margin: 40px auto 0 80px"
>
<n-form-item label="付款账户" path="myAccount">
<span>NaiveUiAdmin@163.com</span>
</n-form-item>
<n-form-item label="收款账户" path="account">
<span>NaiveUiAdmin@qq.com</span>
</n-form-item>
<n-form-item label="收款人姓名" path="name">
<span>Ah jung</span>
</n-form-item>
<n-form-item label="转账金额" path="money">
<span>1980</span>
</n-form-item>
<n-divider />
<n-form-item label="支付密码" path="password">
<n-input type="password" v-model:value="formValue.password" />
</n-form-item>
<div style="margin-left: 80px">
<n-space>
<n-button type="primary" :loading="loading" @click="formSubmit">提交</n-button>
<n-button @click="prevStep">上一步</n-button>
</n-space>
</div>
</n-form>
</template>
<script lang="ts" setup>
import { ref, defineEmits } from 'vue';
import { useMessage } from 'naive-ui';
const form2Ref: any = ref(null);
const message = useMessage();
const loading = ref(false);
const formValue = ref({
password: '086611',
});
const rules = {
password: {
required: true,
message: '请输入支付密码',
trigger: 'blur',
},
};
const emit = defineEmits(['prevStep', 'nextStep']);
function prevStep() {
emit('prevStep');
}
function formSubmit() {
loading.value = true;
form2Ref.value.validate((errors) => {
if (!errors) {
setTimeout(() => {
emit('nextStep');
}, 1500);
} else {
message.error('验证失败,请填写完整信息');
}
});
}
</script>

View File

@@ -1,72 +0,0 @@
<template>
<div>
<n-result status="success" title="操作成功" description="预计两小时内到账" class="step-result">
<template #default>
<div class="information">
<n-grid cols="2 s:2 m:3 l:3 xl:3 2xl:3" responsive="screen" class="my-1">
<n-gi>付款账户</n-gi>
<n-gi>NaiveUiAdmin@163.com</n-gi>
</n-grid>
<n-grid cols="2 s:2 m:3 l:3 xl:3 2xl:3" responsive="screen" class="my-1">
<n-gi>收款账户</n-gi>
<n-gi>xiaoma@qq.com</n-gi>
</n-grid>
<n-grid cols="2 s:2 m:3 l:3 xl:3 2xl:3" responsive="screen" class="my-1">
<n-gi>收款人姓名</n-gi>
<n-gi>啊俊</n-gi>
</n-grid>
<n-grid cols="2 s:2 m:3 l:3 xl:3 2xl:3" responsive="screen" class="my-1">
<n-gi>转账金额</n-gi>
<n-gi><span class="money">1980</span> </n-gi>
</n-grid>
</div>
</template>
<template #footer>
<div class="flex justify-center">
<n-button type="primary" @click="finish" class="mr-4">再转一笔</n-button>
<n-button @click="prevStep">查看账单</n-button>
</div>
</template>
</n-result>
</div>
</template>
<script lang="ts" setup>
import { defineEmits } from 'vue';
const emit = defineEmits(['finish', 'prevStep']);
function prevStep() {
emit('prevStep');
}
function finish() {
emit('finish');
}
</script>
<style lang="less" scoped>
.step-result {
max-width: 560px;
margin: 40px auto 0;
::v-deep(.n-result-content) {
background-color: #fafafa;
padding: 24px 40px;
}
.information {
line-height: 22px;
.ant-row:not(:last-child) {
margin-bottom: 24px;
}
}
.money {
font-family: 'Helvetica Neue', sans-serif;
font-weight: 500;
font-size: 20px;
line-height: 14px;
}
}
</style>

View File

@@ -1,54 +0,0 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="分步表单">
将一个冗长或用户不熟悉的表单任务分成多个步骤指导用户完成
</n-card>
</div>
<n-card :bordered="false" class="mt-4 proCard">
<n-space vertical class="steps" justify="center">
<n-steps :current="currentTab" :status="currentStatus">
<n-step title="填写转账信息" description="确保填写正确" />
<n-step title="确认转账信息" description="确认转账信息" />
<n-step title="完成转账" description="恭喜您,转账成功" />
</n-steps>
<step1 v-if="currentTab === 1" @nextStep="nextStep" />
<step2 v-if="currentTab === 2" @nextStep="nextStep" @prevStep="prevStep" />
<step3 v-if="currentTab === 3" @prevStep="prevStep" @finish="finish" />
</n-space>
</n-card>
</div>
</template>
<script setup>
import { defineComponent, ref } from 'vue';
import step1 from './Step1.vue';
import step2 from './Step2.vue';
import step3 from './Step3.vue';
const currentTab = ref(1);
const currentStatus = ref('process');
function nextStep() {
if (currentTab.value < 3) {
currentTab.value += 1;
}
}
function prevStep() {
if (currentTab.value > 1) {
currentTab.value -= 1;
}
}
function finish() {
currentTab.value = 1;
}
</script>
<style lang="less" scoped>
.steps {
max-width: 750px;
margin: 16px auto;
}
</style>

View File

@@ -1 +0,0 @@
<template> 项目文档 </template>

View File

@@ -23,8 +23,14 @@
column="2"
content-style="padding-right: 20px;"
>
<n-descriptions-item label="用户ID">{{ formValue.id }}</n-descriptions-item>
<n-descriptions-item label="管理员ID">{{ formValue.id }}</n-descriptions-item>
<n-descriptions-item label="用户名"> {{ formValue.username }} </n-descriptions-item>
<n-descriptions-item label="余额">{{
Number(formValue.balance).toFixed(2)
}}</n-descriptions-item>
<n-descriptions-item label="积分">
{{ Number(formValue.integral).toFixed(2) }}
</n-descriptions-item>
<n-descriptions-item label="登录IP">{{
formValue.lastLoginIp
}}</n-descriptions-item>

View File

@@ -89,8 +89,8 @@
show.value = true;
getUserInfo()
.then((res) => {
res.cash.password = '';
formValue.value = res.cash;
formValue.value.password = '';
})
.finally(() => {
show.value = false;

View File

@@ -0,0 +1,311 @@
<template>
<n-grid cols="1" responsive="screen" class="-mt-5">
<n-grid-item>
<n-list>
<n-list-item>
<template #suffix>
<n-button type="primary" text @click="openUpdatePassForm">修改</n-button>
</template>
<n-thing title="绑定微信">
<template #description
><span class="text-gray-400">已绑定微信号xxx</span></template
>
</n-thing>
</n-list-item>
<n-list-item>
<template #suffix>
<n-button type="primary" text @click="openUpdateMobileForm">修改</n-button>
</template>
<n-thing title="绑定抖音">
<template #description
><span class="text-gray-400"
>已绑定抖音号xxx</span
></template
>
</n-thing>
</n-list-item>
</n-list>
</n-grid-item>
</n-grid>
<n-modal
v-model:show="showModal"
:show-icon="false"
preset="dialog"
title="修改登录密码"
:style="{
width: dialogWidth,
}"
>
<n-form :label-width="80" :model="formValue" :rules="rules" ref="formRef">
<n-form-item label="当前密码" path="oldPassword">
<n-input
type="password"
v-model:value="formValue.oldPassword"
placeholder="请输入当前密码"
/>
</n-form-item>
<n-form-item label="新密码" path="newPassword">
<n-input type="password" v-model:value="formValue.newPassword" placeholder="请输入新密码" />
</n-form-item>
<div>
<n-space justify="end">
<n-button @click="showModal = false">取消</n-button>
<n-button type="primary" @click="formSubmit">修改并重新登录</n-button>
</n-space>
</div>
</n-form>
</n-modal>
<n-modal
:block-scroll="false"
:mask-closable="false"
v-model:show="showMobileModal"
:show-icon="false"
preset="dialog"
title="修改手机号"
:style="{
width: dialogWidth,
}"
>
<n-form :label-width="80" :model="formMobileValue" ref="formMobileRef">
<n-form-item label="短信验证码" path="code" v-if="userStore.info?.mobile !== ''">
<n-input-group>
<n-input v-model:value="formMobileValue.code" placeholder="请输入验证码" />
<n-button
type="primary"
ghost
@click="sendMobileCode"
:disabled="isCounting"
:loading="sendLoading"
>
{{ sendLabel }}
</n-button>
</n-input-group>
<template #feedback> 接收号码+86{{ userStore.info?.mobile }} </template>
</n-form-item>
<n-form-item label="换绑手机号" path="mobile">
<n-input v-model:value="formMobileValue.mobile" placeholder="请输入换绑手机号" />
</n-form-item>
<div>
<n-space justify="end">
<n-button @click="showMobileModal = false">取消</n-button>
<n-button type="primary" :loading="formMobileBtnLoading" @click="formMobileSubmit"
>保存更新</n-button
>
</n-space>
</div>
</n-form>
</n-modal>
<n-modal
:block-scroll="false"
:mask-closable="false"
v-model:show="showEmailModal"
:show-icon="false"
preset="dialog"
title="修改邮箱"
:style="{
width: dialogWidth,
}"
>
<n-form :label-width="80" :model="formEmailValue" ref="formEmailRef">
<n-form-item label="邮箱验证码" path="code" v-if="userStore.info?.email !== ''">
<n-input-group>
<n-input v-model:value="formEmailValue.code" placeholder="请输入验证码" />
<n-button
type="primary"
ghost
@click="sendEmailCode"
:disabled="isCounting"
:loading="sendLoading"
>
{{ sendLabel }}
</n-button>
</n-input-group>
<template #feedback> 接收邮箱{{ userStore.info?.email }} </template>
</n-form-item>
<n-form-item label="换绑邮箱" path="email">
<n-input v-model:value="formEmailValue.email" placeholder="请输入换绑邮箱" />
</n-form-item>
<div>
<n-space justify="end">
<n-button @click="showEmailModal = false">取消</n-button>
<n-button type="primary" :loading="formEmailBtnLoading" @click="formEmailSubmit"
>保存更新</n-button
>
</n-space>
</div>
</n-form>
</n-modal>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { useMessage } from 'naive-ui';
import { useRouter, useRoute } from 'vue-router';
import { useSendCode } from '@/hooks/common';
import { adaModalWidth } from '@/utils/hotgo';
import {
updateMemberPwd,
updateMemberMobile,
updateMemberEmail,
SendBindEmail,
SendBindSms,
} from '@/api/system/user';
import { TABS_ROUTES } from '@/store/mutation-types';
import { useUserStore } from '@/store/modules/user';
const { sendLabel, isCounting, loading: sendLoading, activateSend } = useSendCode();
const userStore = useUserStore();
const dialogWidth = ref('75%');
const rules = {
basicName: {
required: true,
message: '请输入网站名称',
trigger: 'blur',
},
};
const formRef: any = ref(null);
const message = useMessage();
const router = useRouter();
const route = useRoute();
const showModal = ref(false);
const formValue = ref({
oldPassword: '',
newPassword: '',
});
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
updateMemberPwd({
oldPassword: formValue.value.oldPassword,
newPassword: formValue.value.newPassword,
})
.then((_res) => {
message.success('更新成功');
userStore.logout().then(() => {
message.success('成功退出登录');
// 移除标签页
localStorage.removeItem(TABS_ROUTES);
router
.replace({
name: 'Login',
query: {
redirect: route.fullPath,
},
})
.finally(() => location.reload());
});
})
.finally(() => {
showModal.value = false;
});
} else {
message.error('验证失败,请填写完整信息');
}
});
}
function openUpdatePassForm() {
message.error('未开放');
return
showModal.value = true;
formValue.value.newPassword = '';
formValue.value.oldPassword = '';
}
const formMobileBtnLoading = ref(false);
const formMobileRef: any = ref(null);
const showMobileModal = ref(false);
const formMobileValue = ref({
mobile: '',
code: '',
});
function formMobileSubmit() {
formMobileRef.value.validate((errors) => {
if (!errors) {
formMobileBtnLoading.value = true;
updateMemberMobile({
mobile: formMobileValue.value.mobile,
code: formMobileValue.value.code,
})
.then((_res) => {
message.success('更新成功');
showMobileModal.value = false;
userStore.GetInfo();
})
.finally(() => {
formMobileBtnLoading.value = false;
});
} else {
message.error('验证失败,请填写完整信息');
}
});
}
function openUpdateMobileForm() {
message.error('未开放');
return
showMobileModal.value = true;
formMobileValue.value.mobile = '';
formMobileValue.value.code = '';
}
const formEmailBtnLoading = ref(false);
const formEmailRef: any = ref(null);
const showEmailModal = ref(false);
const formEmailValue = ref({
email: '',
code: '',
});
function formEmailSubmit() {
formEmailRef.value.validate((errors) => {
if (!errors) {
formEmailBtnLoading.value = true;
updateMemberEmail({
email: formEmailValue.value.email,
code: formEmailValue.value.code,
})
.then((_res) => {
message.success('更新成功');
showEmailModal.value = false;
userStore.GetInfo();
})
.finally(() => {
formEmailBtnLoading.value = false;
});
} else {
message.error('验证失败,请填写完整信息');
}
});
}
function openUpdateEmailForm() {
showEmailModal.value = true;
formEmailValue.value.email = '';
formEmailValue.value.code = '';
}
function sendMobileCode() {
activateSend(SendBindSms());
}
function sendEmailCode() {
activateSend(SendBindEmail());
}
onMounted(async () => {
adaModalWidth(dialogWidth, 580);
});
</script>

View File

@@ -20,16 +20,23 @@
<BasicSetting v-if="type === 1" />
<SafetySetting v-if="type === 2" />
<CashSetting v-if="type === 3" />
<ThirdBind v-if="type === 4" />
</n-card>
</n-grid-item>
</n-grid>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { ref, onMounted } from 'vue';
import BasicSetting from './BasicSetting.vue';
import SafetySetting from './SafetySetting.vue';
import CashSetting from './CashSetting.vue';
import ThirdBind from './ThirdBind.vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const type = ref(1);
const typeTitle = ref('基本设置');
const typeTabList = [
{
@@ -47,10 +54,29 @@
desc: '提现收款账号支付宝设置',
key: 3,
},
{
name: '第三方绑定',
desc: '第三方快捷登录、消息推送',
key: 4,
},
];
const type = ref(1);
const typeTitle = ref('基本设置');
onMounted(() => {
if (router.currentRoute.value.query?.type) {
setDefaultOption();
}
});
function setDefaultOption() {
const key = router.currentRoute.value.query.type as unknown as number;
if (key !== undefined && key > 0) {
for (const item of typeTabList) {
if (item.key == key) {
switchType(item);
}
}
}
}
function switchType(e) {
type.value = e.key;

View File

@@ -10,13 +10,12 @@
type="card"
class="card-tabs"
:value="defaultTab"
size="large"
animated
@before-leave="handleBeforeLeave"
>
<n-tab-pane name="1" tab="通知"> <List :type="defaultTab" /></n-tab-pane>
<n-tab-pane name="2" tab="公告"> <List :type="defaultTab" /> </n-tab-pane>
<n-tab-pane name="3" tab="私信"> <List :type="defaultTab" /> </n-tab-pane>
<n-tab-pane name="1" tab="系统通知"> <List :type="defaultTab" /></n-tab-pane>
<n-tab-pane name="2" tab="系统公告"> <List :type="defaultTab" /> </n-tab-pane>
<n-tab-pane name="3" tab="私信消息"> <List :type="defaultTab" /> </n-tab-pane>
</n-tabs>
</n-card>
</div>

View File

@@ -1,50 +0,0 @@
import { h } from 'vue';
import { NAvatar } from 'naive-ui';
export const columns = [
{
title: 'id',
key: 'id',
width: 100,
},
{
title: '名称',
key: 'name',
width: 100,
},
{
title: '头像',
key: 'avatar',
width: 100,
render(row) {
return h(NAvatar, {
size: 48,
src: row.avatar,
});
},
},
{
title: '地址',
key: 'address',
auth: ['basic_list'], // 同时根据权限控制是否显示
ifShow: (_column) => {
return true; // 根据业务控制是否显示
},
width: 150,
},
{
title: '开始日期',
key: 'beginTime',
width: 160,
},
{
title: '结束日期',
key: 'endTime',
width: 160,
},
{
title: '创建时间',
key: 'date',
width: 100,
},
];

View File

@@ -1,347 +0,0 @@
<template>
<n-card :bordered="false" class="proCard">
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset">
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
>
<template #tableTitle>
<n-button type="primary" @click="addTable">
<template #icon>
<n-icon>
<PlusOutlined />
</n-icon>
</template>
添加
</n-button>
</template>
<template #toolbar>
<n-button type="primary" @click="reloadTable">刷新数据</n-button>
</template>
</BasicTable>
<n-modal v-model:show="showModal" :show-icon="false" preset="dialog" title="添加">
<n-form
:model="formParams"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-form-item label="名称" path="name">
<n-input placeholder="请输入名称" v-model:value="formParams.name" />
</n-form-item>
<n-form-item label="地址" path="address">
<n-input type="textarea" placeholder="请输入地址" v-model:value="formParams.address" />
</n-form-item>
<n-form-item label="日期" path="date">
<n-date-picker type="datetime" placeholder="请选择日期" v-model:value="formParams.date" />
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="() => (showModal = false)">取消</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">确定</n-button>
</n-space>
</template>
</n-modal>
</n-card>
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { getTableList } from '@/api/table/list';
import { columns } from './columns';
import { PlusOutlined } from '@vicons/antd';
import { useRouter } from 'vue-router';
const rules = {
name: {
required: true,
trigger: ['blur', 'input'],
message: '请输入名称',
},
address: {
required: true,
trigger: ['blur', 'input'],
message: '请输入地址',
},
date: {
type: 'number',
required: true,
trigger: ['blur', 'change'],
message: '请选择日期',
},
};
const schemas: FormSchema[] = [
{
field: 'name',
labelMessage: '这是一个提示',
component: 'NInput',
label: '姓名',
componentProps: {
placeholder: '请输入姓名',
onInput: (e: any) => {
console.log(e);
},
},
rules: [{ required: true, message: '请输入姓名', trigger: ['blur'] }],
},
{
field: 'mobile',
component: 'NInputNumber',
label: '手机',
componentProps: {
placeholder: '请输入手机号码',
showButton: false,
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'type',
component: 'NSelect',
label: '类型',
componentProps: {
placeholder: '请选择类型',
options: [
{
label: '舒适性',
value: 1,
},
{
label: '经济性',
value: 2,
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeDate',
component: 'NDatePicker',
label: '预约时间',
defaultValue: 1183135260000,
componentProps: {
type: 'date',
clearable: true,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeTime',
component: 'NTimePicker',
label: '停留时间',
componentProps: {
clearable: true,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
label: '状态',
//插槽
slot: 'statusSlot',
},
{
field: 'makeProject',
component: 'NCheckbox',
label: '预约项目',
componentProps: {
placeholder: '请选择预约项目',
options: [
{
label: '种牙',
value: 1,
},
{
label: '补牙',
value: 2,
},
{
label: '根管',
value: 3,
},
],
onUpdateChecked: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeSource',
component: 'NRadioGroup',
label: '来源',
componentProps: {
options: [
{
label: '网上',
value: 1,
},
{
label: '门店',
value: 2,
},
],
onUpdateChecked: (e: any) => {
console.log(e);
},
},
},
];
const router = useRouter();
const formRef: any = ref(null);
const message = useMessage();
const actionRef = ref();
const showModal = ref(false);
const formBtnLoading = ref(false);
const formParams = reactive({
name: '',
address: '',
date: null,
});
const params = ref({
pageSize: 5,
name: 'xiaoMa',
});
const actionColumn = reactive({
width: 220,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '删除',
icon: 'ic:outline-delete-outline',
onClick: handleDelete.bind(null, record),
// 根据业务控制是否显示 isShow 和 auth 是并且关系
ifShow: () => {
return true;
},
// 根据权限控制是否显示: 有权限,会显示,支持多个
auth: ['basic_list'],
},
{
label: '编辑',
onClick: handleEdit.bind(null, record),
ifShow: () => {
return true;
},
auth: ['basic_list'],
},
],
dropDownActions: [
{
label: '启用',
key: 'enabled',
// 根据业务控制是否显示: 非enable状态的不显示启用按钮
ifShow: () => {
return true;
},
},
{
label: '禁用',
key: 'disabled',
ifShow: () => {
return true;
},
},
],
select: (key) => {
message.info(`您点击了,${key} 按钮`);
},
});
},
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
function addTable() {
showModal.value = true;
}
const loadDataTable = async (res) => {
return await getTableList({ ...formParams, ...params.value, ...res });
};
function onCheckedRow(rowKeys) {
console.log(rowKeys);
}
function reloadTable() {
actionRef.value.reload();
}
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
message.success('添加成功');
setTimeout(() => {
showModal.value = false;
reloadTable();
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
function handleEdit(record: Recordable) {
console.log('点击了编辑', record);
router.push({ name: 'basic-info', params: { id: record.id } });
}
function handleDelete(record: Recordable) {
console.log('点击了删除', record);
message.info('点击了删除');
}
function handleSubmit(values: Recordable) {
console.log(values);
reloadTable();
}
function handleReset(values: Recordable) {
console.log(values);
}
</script>
<style lang="less" scoped></style>

View File

@@ -1,34 +0,0 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="基础详情"> 基础详情有时也用于显示只读信息 </n-card>
</div>
<n-card :bordered="false" class="proCard mt-4" size="small" :segmented="{ content: true }">
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item>
<template #label>收款人姓名</template>
啊俊
</n-descriptions-item>
<n-descriptions-item label="收款账户">NaiveUiAdmin@qq.com</n-descriptions-item>
<n-descriptions-item label="付款类型">支付宝</n-descriptions-item>
<n-descriptions-item label="付款账户">NaiveUiAdmin@163.com</n-descriptions-item>
<n-descriptions-item label="转账金额">1980.00</n-descriptions-item>
<n-descriptions-item label="状态">
<n-tag type="success"> 已到账</n-tag>
</n-descriptions-item>
</n-descriptions>
</n-card>
</div>
</template>
<script>
import { defineComponent } from 'vue';
export default defineComponent({
setup() {
return {};
},
});
</script>
<style lang="less" scoped></style>

View File

@@ -97,19 +97,19 @@
<div class="flex-initial mx-2">
<a href="javascript:">
<n-icon size="24" color="#2d8cf0">
<LogoGithub />
<LogoWechat />
</n-icon>
</a>
</div>
<div class="flex-initial mx-2">
<a href="javascript:">
<n-icon size="24" color="#2d8cf0">
<LogoFacebook />
<LogoTiktok />
</n-icon>
</a>
</div>
<div class="flex-initial" style="margin-left: auto">
<a href="javascript:">注册账号</a>
<a @click="handleRegister">注册账号</a>
</div>
</div>
</n-form-item>
@@ -125,7 +125,7 @@
import { useUserStore } from '@/store/modules/user';
import { useMessage, useLoadingBar } from 'naive-ui';
import { ResultEnum } from '@/enums/httpEnum';
import { PersonOutline, LockClosedOutline, LogoGithub, LogoFacebook } from '@vicons/ionicons5';
import { PersonOutline, LockClosedOutline, LogoWechat, LogoTiktok } from '@vicons/ionicons5';
import { PageEnum } from '@/enums/pageEnum';
import { SafetyCertificateOutlined } from '@vicons/antd';
import { GetCaptcha } from '@/api/base';
@@ -163,7 +163,6 @@
};
const userStore = useUserStore();
const router = useRouter();
const route = useRoute();
@@ -213,7 +212,13 @@
setTimeout(function () {
refreshCode();
});
console.log('window.location.href',route.path);
});
function handleRegister() {
message.success('即将开放,请稍后');
return;
}
</script>
<style lang="less" scoped>

View File

@@ -122,7 +122,7 @@
const showModal = ref(false);
const actionColumn = reactive({
width: 120,
width: 180,
title: '操作',
key: 'action',
fixed: 'right',

View File

@@ -1,14 +1,13 @@
import { h, ref } from 'vue';
import { NAvatar, NImage, NTag, NSwitch, NRate, NButton } from 'naive-ui';
import { NTag, NButton } from 'naive-ui';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { Dicts } from '@/api/dict/dict';
import { isArray, isNullObject } from '@/utils/is';
import { getFileExt } from '@/utils/urlUtils';
import { defRangeShortcuts, defShortcuts, formatToDate } from '@/utils/dateUtil';
import { isNullObject } from '@/utils/is';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { format } from 'date-fns';
import { getOptionLabel, getOptionTag, Options, errorImg } from '@/utils/hotgo';
import { getOptionLabel, getOptionTag, Options } from '@/utils/hotgo';
import { renderIcon, renderTooltip } from '@/utils';
import { HelpCircleOutline } from '@vicons/ionicons5';

View File

@@ -0,0 +1,147 @@
<template>
<div>
<n-spin :show="loading" description="请稍候...">
<n-modal
v-model:show="isShowModal"
:show-icon="false"
preset="dialog"
title="变更余额"
:style="{
width: dialogWidth,
}"
>
<n-alert :show-icon="false" type="info">
通过扣除或增加你的余额来为
<b> {{ params.realName }}</b> 加款或扣款当扣款方余额不足时则会操作失败
</n-alert>
<n-form
:model="params"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-form-item label="管理员ID" path="id">
<n-input v-model:value="params.id" :disabled="true" />
</n-form-item>
<n-form-item label="TA的余额" path="balance">
<n-input placeholder="请输入" v-model:value="params.balance" :disabled="true" />
</n-form-item>
<n-form-item label="操作方式" path="operateMode">
<n-radio-group v-model:value="params.operateMode" name="operateMode">
<n-radio-button
v-for="status in operateModes"
:key="status.value"
:value="status.value"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
<n-form-item label="操作数量" path="num">
<n-input placeholder="请输入操作数量" v-model:value="params.num" />
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="closeForm">取消</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">确定</n-button>
</n-space>
</template>
</n-modal>
</n-spin>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, computed, watch } from 'vue';
import {
addRules as rules,
addState as State,
addNewState as newState,
operateModes,
} from './model';
import { useMessage } from 'naive-ui';
import { adaModalWidth } from '@/utils/hotgo';
import { GetMemberView, AddMemberBalance } from '@/api/org/user';
const emit = defineEmits(['reloadTable', 'updateShowModal']);
interface Props {
showModal: boolean;
formParams?: State;
}
const props = withDefaults(defineProps<Props>(), {
showModal: false,
formParams: () => {
return newState(null);
},
});
const isShowModal = computed({
get: () => {
return props.showModal;
},
set: (value) => {
emit('updateShowModal', value);
},
});
const loading = ref(false);
const params = ref<State>(props.formParams);
const message = useMessage();
const formRef = ref<any>({});
const dialogWidth = ref('75%');
const formBtnLoading = ref(false);
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
AddMemberBalance(params.value).then((_res) => {
message.success('操作成功');
setTimeout(() => {
isShowModal.value = false;
emit('reloadTable');
});
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
onMounted(async () => {
adaModalWidth(dialogWidth);
});
function closeForm() {
isShowModal.value = false;
}
function loadForm(value) {
loading.value = true;
GetMemberView({ id: value.id })
.then((res) => {
params.value = res;
params.value.operateMode = 1;
})
.finally(() => {
loading.value = false;
});
}
watch(
() => props.formParams,
(value) => {
loadForm(value);
}
);
</script>
<style lang="less"></style>

View File

@@ -0,0 +1,147 @@
<template>
<div>
<n-spin :show="loading" description="请稍候...">
<n-modal
v-model:show="isShowModal"
:show-icon="false"
preset="dialog"
title="变更积分"
:style="{
width: dialogWidth,
}"
>
<n-alert :show-icon="false" type="info">
通过扣除或增加你的积分来为
<b> {{ params.realName }}</b> 加款或扣款当扣款方积分不足时则会操作失败
</n-alert>
<n-form
:model="params"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-form-item label="管理员ID" path="id">
<n-input v-model:value="params.id" :disabled="true" />
</n-form-item>
<n-form-item label="TA的积分" path="integral">
<n-input placeholder="请输入" v-model:value="params.integral" :disabled="true" />
</n-form-item>
<n-form-item label="操作方式" path="operateMode">
<n-radio-group v-model:value="params.operateMode" name="operateMode">
<n-radio-button
v-for="status in operateModes"
:key="status.value"
:value="status.value"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
<n-form-item label="操作数量" path="num">
<n-input placeholder="请输入操作数量" v-model:value="params.num" />
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="closeForm">取消</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">确定</n-button>
</n-space>
</template>
</n-modal>
</n-spin>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, computed, watch } from 'vue';
import {
addRules as rules,
addState as State,
addNewState as newState,
operateModes,
} from './model';
import { useMessage } from 'naive-ui';
import { adaModalWidth } from '@/utils/hotgo';
import { GetMemberView, AddMemberIntegral } from '@/api/org/user';
const emit = defineEmits(['reloadTable', 'updateShowModal']);
interface Props {
showModal: boolean;
formParams?: State;
}
const props = withDefaults(defineProps<Props>(), {
showModal: false,
formParams: () => {
return newState(null);
},
});
const isShowModal = computed({
get: () => {
return props.showModal;
},
set: (value) => {
emit('updateShowModal', value);
},
});
const loading = ref(false);
const params = ref<State>(props.formParams);
const message = useMessage();
const formRef = ref<any>({});
const dialogWidth = ref('75%');
const formBtnLoading = ref(false);
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
AddMemberIntegral(params.value).then((_res) => {
message.success('操作成功');
setTimeout(() => {
isShowModal.value = false;
emit('reloadTable');
});
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
onMounted(async () => {
adaModalWidth(dialogWidth);
});
function closeForm() {
isShowModal.value = false;
}
function loadForm(value) {
loading.value = true;
GetMemberView({ id: value.id })
.then((res) => {
params.value = res;
params.value.operateMode = 1;
})
.finally(() => {
loading.value = false;
});
}
watch(
() => props.formParams,
(value) => {
loadForm(value);
}
);
</script>
<style lang="less"></style>

View File

@@ -4,9 +4,9 @@ import { formatBefore } from '@/utils/dateUtil';
export const columns = [
{
title: 'ID',
title: '管理员ID',
key: 'id',
width: 60,
width: 100,
},
{
title: '用户名',
@@ -21,7 +21,7 @@ export const columns = [
{
title: '头像',
key: 'avatar',
width: 50,
width: 70,
render(row) {
if (row.avatar !== '') {
return h(NAvatar, {
@@ -84,6 +84,22 @@ export const columns = [
);
},
},
{
title: '余额',
key: 'balance',
width: 120,
render(row) {
return '¥' + Number(row.balance).toFixed(2);
},
},
{
title: '积分',
key: 'integral',
width: 120,
render(row) {
return Number(row.integral).toFixed(2);
},
},
{
title: '状态',
key: 'status',

View File

@@ -0,0 +1,470 @@
<template>
<div>
<BasicForm
@register="register"
@submit="handleSubmit"
@reset="handleReset"
@keyup.enter="handleSubmit"
ref="searchFormRef"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:openChecked="true"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1800"
>
<template #tableTitle>
<n-button
type="primary"
@click="addTable"
class="min-left-space"
v-if="hasPermission(['/member/edit'])"
>
<template #icon>
<n-icon>
<PlusOutlined />
</n-icon>
</template>
添加用户
</n-button>
<n-button
type="error"
@click="batchDelete"
:disabled="batchDeleteDisabled"
class="min-left-space"
v-if="hasPermission(['/member/delete'])"
>
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
</template>
</BasicTable>
<n-modal
v-model:show="showModal"
:show-icon="false"
preset="dialog"
:title="formParams?.id > 0 ? '编辑用户 #' + formParams?.id : '添加用户'"
:style="{
width: dialogWidth,
}"
>
<n-form
:model="formParams"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="姓名" path="realName">
<n-input placeholder="请输入姓名" v-model:value="formParams.realName" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="用户名" path="username">
<n-input placeholder="请输入登录用户名" v-model:value="formParams.username" />
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="绑定角色" path="roleId">
<n-tree-select
key-field="id"
:options="options.role"
:default-value="formParams.roleId"
:default-expand-all="true"
@update:value="handleUpdateRoleValue"
/>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="所属部门" path="deptId">
<n-tree-select
key-field="id"
:options="options.dept"
:default-value="formParams.deptId"
:default-expand-all="true"
@update:value="handleUpdateDeptValue"
/>
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="绑定岗位" path="postIds">
<n-select
:default-value="formParams.postIds"
multiple
:options="options.post"
@update:value="handleUpdatePostValue"
/>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="密码" path="password">
<n-input
type="password"
:placeholder="formParams.id === 0 ? '请输入' : '不填则不修改'"
v-model:value="formParams.password"
/>
</n-form-item>
</n-gi>
</n-grid>
<n-divider title-placement="left">填写更多信息(可选)</n-divider>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="手机号" path="mobile">
<n-input placeholder="请输入" v-model:value="formParams.mobile" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="邮箱" path="email">
<n-input placeholder="请输入" v-model:value="formParams.email" />
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="性别" path="sex">
<n-radio-group v-model:value="formParams.sex" name="sex">
<n-radio-button
v-for="status in sexOptions"
:key="status.value"
:value="status.value"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="状态" path="status">
<n-radio-group v-model:value="formParams.status" name="status">
<n-radio-button
v-for="status in statusOptions"
:key="status.value"
:value="status.value"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
</n-grid>
<n-form-item label="备注" path="remark">
<n-input type="textarea" placeholder="请输入备注" v-model:value="formParams.remark" />
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="() => (showModal = false)">取消</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">确定</n-button>
</n-space>
</template>
</n-modal>
<AddBalance
@reloadTable="reloadTable"
@updateShowModal="updateBalanceShowModal"
:showModal="showBalanceModal"
:formParams="formParams"
/>
<AddIntegral
@reloadTable="reloadTable"
@updateShowModal="updateIntegralShowModal"
:showModal="showIntegralModal"
:formParams="formParams"
/>
</div>
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { SelectOption, TreeSelectOption, useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm } from '@/components/Form/index';
import { Delete, Edit, List, Status, ResetPwd } from '@/api/org/user';
import { columns } from './columns';
import { PlusOutlined, DeleteOutlined } from '@vicons/antd';
import { sexOptions, statusOptions } from '@/enums/optionsiEnum';
import { adaModalWidth } from '@/utils/hotgo';
import { getRandomString } from '@/utils/charset';
import { cloneDeep } from 'lodash-es';
import AddBalance from './addBalance.vue';
import AddIntegral from './addIntegral.vue';
import { addNewState, addState, options, register, defaultState } from './model';
import { usePermission } from '@/hooks/web/usePermission';
interface Props {
type?: string;
}
const props = withDefaults(defineProps<Props>(), {
type: '-1',
});
const rules = {
username: {
required: true,
trigger: ['blur', 'input'],
message: '请输入用户名',
},
};
const { hasPermission } = usePermission();
const showIntegralModal = ref(false);
const showBalanceModal = ref(false);
const message = useMessage();
const actionRef = ref();
const dialog = useDialog();
const showModal = ref(false);
const formBtnLoading = ref(false);
const searchFormRef = ref<any>({});
const formRef = ref<any>({});
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
const dialogWidth = ref('50%');
const formParams = ref<any>();
const actionColumn = reactive({
width: 220,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '已启用',
onClick: handleStatus.bind(null, record, 2),
ifShow: () => {
return record.status === 1 && record.id !== 1;
},
auth: ['/member/status'],
},
{
label: '已禁用',
onClick: handleStatus.bind(null, record, 1),
ifShow: () => {
return record.status === 2 && record.id !== 1;
},
auth: ['/member/status'],
},
{
label: '编辑',
onClick: handleEdit.bind(null, record),
ifShow: () => {
return record.id !== 1;
},
auth: ['/member/edit'],
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
ifShow: () => {
return record.id !== 1;
},
auth: ['/member/delete'],
},
],
dropDownActions:
record.id === 1
? []
: [
{
label: '重置密码',
key: 0,
},
{
label: '变更余额',
key: 100,
},
{
label: '变更积分',
key: 101,
},
],
select: (key) => {
if (key === 0) {
return handleResetPwd(record);
}
if (key === 100) {
return handleAddBalance(record);
}
if (key === 101) {
return handleAddIntegral(record);
}
},
});
},
});
function addTable() {
showModal.value = true;
formParams.value = cloneDeep(defaultState);
}
const loadDataTable = async (res) => {
adaModalWidth(dialogWidth);
return await List({ ...res, ...searchFormRef.value?.formModel, ...{ roleId: props.type } });
};
function onCheckedRow(rowKeys) {
batchDeleteDisabled.value = rowKeys.length <= 0;
checkedIds.value = rowKeys;
}
function reloadTable() {
actionRef.value.reload();
}
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
Edit(formParams.value).then((_res) => {
message.success('操作成功');
setTimeout(() => {
showModal.value = false;
reloadTable();
});
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
function handleEdit(record: Recordable) {
showModal.value = true;
formParams.value = cloneDeep(record);
}
function handleResetPwd(record: Recordable) {
record.password = getRandomString(12);
dialog.warning({
title: '警告',
content: '你确定要重置密码?\r\n重置成功后密码为' + record.password + '\r\n 请先保存',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
ResetPwd(record).then((_res) => {
message.success('操作成功');
reloadTable();
});
},
});
}
function handleDelete(record: Recordable) {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete(record).then((_res) => {
message.success('操作成功');
reloadTable();
});
},
});
}
function batchDelete() {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete({ id: checkedIds.value }).then((_res) => {
message.success('操作成功');
reloadTable();
});
},
});
}
function handleSubmit(_values: Recordable) {
reloadTable();
}
function handleReset(_values: Recordable) {
reloadTable();
}
function handleStatus(record: Recordable, status) {
Status({ id: record.id, status: status }).then((_res) => {
message.success('操作成功');
setTimeout(() => {
reloadTable();
});
});
}
function handleUpdateDeptValue(
value: string | number | Array<string | number> | null,
_option: TreeSelectOption | null | Array<TreeSelectOption | null>
) {
formParams.value.deptId = Number(value);
}
function handleUpdateRoleValue(
value: string | number | Array<string | number> | null,
_option: SelectOption | null | Array<SelectOption | null>
) {
formParams.value.roleId = Number(value);
}
function handleUpdatePostValue(
value: string | number | Array<string | number> | null,
_option: SelectOption | null | Array<SelectOption | null>
) {
formParams.value.postIds = value;
}
function updateBalanceShowModal(value) {
showBalanceModal.value = value;
}
function handleAddBalance(record: Recordable) {
showBalanceModal.value = true;
formParams.value = addNewState(record as addState);
}
function updateIntegralShowModal(value) {
showIntegralModal.value = value;
}
function handleAddIntegral(record: Recordable) {
showIntegralModal.value = true;
formParams.value = addNewState(record as addState);
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,214 @@
import { cloneDeep } from 'lodash-es';
import { ref } from 'vue';
import { getDeptOption } from '@/api/org/dept';
import { getRoleOption } from '@/api/system/role';
import { getPostOption } from '@/api/org/post';
import { FormSchema, useForm } from '@/components/Form';
import { statusOptions } from '@/enums/optionsiEnum';
import { defRangeShortcuts } from '@/utils/dateUtil';
// 增加余额/积分.
export interface addState {
id: number;
username: string;
realName: string;
integral: number;
balance: number;
operateMode: number;
num: number | null;
}
export const addDefaultState = {
id: 0,
realName: '',
username: '',
integral: 0,
balance: 0,
operateMode: 1,
num: null,
};
export function addNewState(state: addState | null): addState {
if (state !== null) {
return cloneDeep(state);
}
return cloneDeep(addDefaultState);
}
export const operateModes = [
{
value: 1,
label: '加款',
},
{
value: 2,
label: '扣款',
},
];
export const addRules = {};
// 用户列表.
export const defaultState = {
id: 0,
roleId: null,
realName: '',
username: '',
password: '',
deptId: null,
postIds: null,
mobile: '',
email: '',
sex: 1,
leader: '',
phone: '',
sort: 0,
status: 1,
createdAt: '',
updatedAt: '',
};
export interface State {
id: number;
roleId: number | null;
realName: string;
username: string;
password: string;
deptId: number | null;
postIds: any;
mobile: string;
email: string;
sex: number;
leader: string;
phone: string;
sort: number;
status: number;
createdAt: string;
updatedAt: string;
}
const schemas: FormSchema[] = [
{
field: 'username',
component: 'NInput',
label: '用户名',
componentProps: {
placeholder: '请输入用户名',
onUpdateValue: (e: any) => {
console.log(e);
},
},
rules: [{ message: '请输入用户名', trigger: ['blur'] }],
},
{
field: 'realName',
component: 'NInput',
label: '姓名',
componentProps: {
placeholder: '请输入姓名',
showButton: false,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'mobile',
component: 'NInputNumber',
label: '手机号',
componentProps: {
placeholder: '请输入手机号码',
showButton: false,
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'email',
component: 'NInput',
label: '邮箱',
componentProps: {
placeholder: '请输入邮箱地址',
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
component: 'NSelect',
label: '状态',
defaultValue: null,
componentProps: {
placeholder: '请选择类型',
options: statusOptions,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'created_at',
component: 'NDatePicker',
label: '创建时间',
componentProps: {
type: 'datetimerange',
clearable: true,
shortcuts: defRangeShortcuts(),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
];
export const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
export const options = ref<any>({
role: [],
roleTabs: [{ id: -1, name: '全部' }],
dept: [],
post: [],
});
async function loadOptions() {
const dept = await getDeptOption();
if (dept.list !== undefined) {
options.value.dept = dept.list;
}
const role = await getRoleOption();
if (role.list !== undefined) {
options.value.role = role.list;
treeDataToCompressed(role.list);
}
const post = await getPostOption();
if (post.list !== undefined && post.list.length > 0) {
for (let i = 0; i < post.list.length; i++) {
post.list[i].label = post.list[i].name;
post.list[i].value = post.list[i].id;
}
options.value.post = post.list;
}
}
function treeDataToCompressed(source) {
for (const i in source) {
options.value.roleTabs.push(source[i]);
source[i].children && source[i].children.length > 0
? treeDataToCompressed(source[i].children)
: ''; // 子级递归
}
return options.value.roleTabs;
}
await loadOptions();

View File

@@ -1,545 +1,45 @@
<template>
<div>
<n-card :bordered="false" class="proCard" title="后台用户">
<BasicForm
@register="register"
@submit="handleSubmit"
@reset="handleReset"
@keyup.enter="handleSubmit"
ref="searchFormRef"
<div class="n-layout-page-header">
<n-card :bordered="false" title="后台用户" />
</div>
<n-card :bordered="false" class="proCard">
<n-tabs
type="card"
class="card-tabs"
:value="defaultTab"
animated
@before-leave="handleBeforeLeave"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:openChecked="true"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
>
<template #tableTitle>
<n-button type="primary" @click="addTable" class="min-left-space">
<template #icon>
<n-icon>
<PlusOutlined />
</n-icon>
</template>
添加用户
</n-button>
<n-button
type="error"
@click="batchDelete"
:disabled="batchDeleteDisabled"
class="min-left-space"
>
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
</template>
</BasicTable>
<n-modal
v-model:show="showModal"
:show-icon="false"
preset="dialog"
:title="formParams?.id > 0 ? '编辑用户 #' + formParams?.id : '添加用户'"
:style="{
width: dialogWidth,
}"
>
<n-form
:model="formParams"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="80"
class="py-4"
<n-tab-pane
:name="item.id.toString()"
:tab="item.name"
v-for="item in options.roleTabs"
:key="item.key"
>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="姓名" path="realName">
<n-input placeholder="请输入姓名" v-model:value="formParams.realName" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="用户名" path="username">
<n-input placeholder="请输入登录用户名" v-model:value="formParams.username" />
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="绑定角色" path="roleId">
<n-tree-select
key-field="id"
:options="roleList"
:default-value="formParams.roleId"
:default-expand-all="true"
@update:value="handleUpdateRoleValue"
/>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="所属部门" path="deptId">
<n-tree-select
key-field="id"
:options="deptList"
:default-value="formParams.deptId"
:default-expand-all="true"
@update:value="handleUpdateDeptValue"
/>
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="绑定岗位" path="postIds">
<n-select
:default-value="formParams.postIds"
multiple
:options="postList"
@update:value="handleUpdatePostValue"
/>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="密码" path="password">
<n-input
type="password"
:placeholder="formParams.id === 0 ? '请输入' : '不填则不修改'"
v-model:value="formParams.password"
/>
</n-form-item>
</n-gi>
</n-grid>
<n-divider title-placement="left">填写更多信息(可选)</n-divider>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="手机号" path="mobile">
<n-input placeholder="请输入" v-model:value="formParams.mobile" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="邮箱" path="email">
<n-input placeholder="请输入" v-model:value="formParams.email" />
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="性别" path="sex">
<n-radio-group v-model:value="formParams.sex" name="sex">
<n-radio-button
v-for="status in sexOptions"
:key="status.value"
:value="status.value"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="状态" path="status">
<n-radio-group v-model:value="formParams.status" name="status">
<n-radio-button
v-for="status in statusOptions"
:key="status.value"
:value="status.value"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
</n-grid>
<n-form-item label="备注" path="remark">
<n-input type="textarea" placeholder="请输入备注" v-model:value="formParams.remark" />
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="() => (showModal = false)">取消</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">确定</n-button>
</n-space>
</template>
</n-modal>
<List :type="defaultTab" />
</n-tab-pane>
</n-tabs>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { SelectOption, TreeSelectOption, useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { Delete, Edit, List, Status, ResetPwd } from '@/api/org/user';
import { columns } from './columns';
import { PlusOutlined, DeleteOutlined } from '@vicons/antd';
import { sexOptions, statusOptions } from '@/enums/optionsiEnum';
import { getDeptList } from '@/api/org/dept';
import { getRoleList } from '@/api/system/role';
import { getPostList } from '@/api/org/post';
import { adaModalWidth } from '@/utils/hotgo';
import { getRandomString } from '@/utils/charset';
import { cloneDeep } from 'lodash-es';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { onMounted, ref } from 'vue';
import List from './list.vue';
import { useRouter } from 'vue-router';
import { options } from './model';
const params = ref<any>({
pageSize: 10,
name: '',
code: '',
status: null,
const router = useRouter();
const defaultTab = ref('-1');
onMounted(() => {
if (router.currentRoute.value.query?.type) {
defaultTab.value = router.currentRoute.value.query.type as string;
}
});
const rules = {
username: {
required: true,
trigger: ['blur', 'input'],
message: '请输入用户名',
},
};
const schemas: FormSchema[] = [
{
field: 'username',
component: 'NInput',
label: '用户名',
componentProps: {
placeholder: '请输入用户名',
onUpdateValue: (e: any) => {
console.log(e);
},
},
rules: [{ message: '请输入用户名', trigger: ['blur'] }],
},
{
field: 'realName',
component: 'NInput',
label: '姓名',
componentProps: {
placeholder: '请输入姓名',
showButton: false,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'mobile',
component: 'NInputNumber',
label: '手机号',
componentProps: {
placeholder: '请输入手机号码',
showButton: false,
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'email',
component: 'NInput',
label: '邮箱',
componentProps: {
placeholder: '请输入邮箱地址',
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
component: 'NSelect',
label: '状态',
defaultValue: null,
componentProps: {
placeholder: '请选择类型',
options: statusOptions,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'created_at',
component: 'NDatePicker',
label: '创建时间',
componentProps: {
type: 'datetimerange',
clearable: true,
shortcuts: defRangeShortcuts(),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
];
const message = useMessage();
const actionRef = ref();
const dialog = useDialog();
const showModal = ref(false);
const formBtnLoading = ref(false);
const searchFormRef = ref<any>({});
const formRef = ref<any>({});
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
const deptList = ref<any>([]);
const roleList = ref<any>([]);
const postList = ref<any>([]);
const dialogWidth = ref('50%');
const defaultState = {
id: 0,
roleId: null,
realName: '',
username: '',
password: '',
deptId: null,
postIds: null,
mobile: '',
email: '',
sex: 1,
leader: '',
phone: '',
sort: 0,
status: 1,
createdAt: '',
updatedAt: '',
};
let formParams = ref<any>();
const actionColumn = reactive({
width: 220,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
},
],
dropDownActions: [
{
label: '重置密码',
key: 0,
},
{
label: '设为启用',
key: 1,
},
{
label: '设为禁用',
key: 2,
},
],
select: (key) => {
if (key === 0) {
return handleResetPwd(record);
}
if (key === 1 || key === 2) {
return updateStatus(record.id, key);
}
},
});
},
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
function addTable() {
showModal.value = true;
formParams.value = cloneDeep(defaultState);
}
const loadDataTable = async (res) => {
adaModalWidth(dialogWidth);
const deptLists = await getDeptList({});
deptList.value = deptLists.list;
if (deptList.value === undefined || deptList.value === null) {
deptList.value = [];
}
roleList.value = [];
let roleLists = await getRoleList({ pageSize: 100 });
if (roleLists.list === undefined || roleLists.list === null) {
roleList.value = [];
} else {
roleList.value = roleLists.list;
}
postList.value = [];
let postLists = await getPostList();
if (postLists.list === undefined || postLists.list === null) {
postLists = [];
} else {
postLists = postLists.list;
}
if (postLists.length > 0) {
for (let i = 0; i < postLists.length; i++) {
postList.value[i] = {};
postList.value[i].label = postLists[i].name;
postList.value[i].value = postLists[i].id;
}
}
return await List({ ...params.value, ...res, ...searchFormRef.value?.formModel });
};
function onCheckedRow(rowKeys) {
batchDeleteDisabled.value = rowKeys.length <= 0;
checkedIds.value = rowKeys;
}
function reloadTable() {
actionRef.value.reload();
}
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
Edit(formParams.value).then((_res) => {
message.success('操作成功');
setTimeout(() => {
showModal.value = false;
reloadTable();
});
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
function handleEdit(record: Recordable) {
showModal.value = true;
formParams.value = cloneDeep(record);
}
function handleResetPwd(record: Recordable) {
record.password = getRandomString(12);
dialog.warning({
title: '警告',
content: '你确定要重置密码?\r\n重置成功后密码为' + record.password + '\r\n 请先保存',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
ResetPwd(record).then((_res) => {
message.success('操作成功');
reloadTable();
});
},
onNegativeClick: () => {
// message.error('取消');
},
});
}
function handleDelete(record: Recordable) {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete(record).then((_res) => {
message.success('操作成功');
reloadTable();
});
},
onNegativeClick: () => {
// message.error('取消');
},
});
}
function batchDelete() {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete({ id: checkedIds.value }).then((_res) => {
message.success('操作成功');
reloadTable();
});
},
onNegativeClick: () => {
// message.error('取消');
},
});
}
function handleSubmit(values: Recordable) {
console.log(values);
params.value = values;
reloadTable();
}
function handleReset(values: Recordable) {
params.value = values;
reloadTable();
}
function updateStatus(id, status) {
Status({ id: id, status: status }).then((_res) => {
message.success('操作成功');
setTimeout(() => {
reloadTable();
});
});
}
function handleUpdateDeptValue(
value: string | number | Array<string | number> | null,
_option: TreeSelectOption | null | Array<TreeSelectOption | null>
) {
formParams.value.deptId = value;
}
function handleUpdateRoleValue(
value: string | number | Array<string | number> | null,
_option: SelectOption | null | Array<SelectOption | null>
) {
formParams.value.roleId = value;
}
function handleUpdatePostValue(
value: string | number | Array<string | number> | null,
_option: SelectOption | null | Array<SelectOption | null>
) {
formParams.value.postIds = value;
function handleBeforeLeave(tabName: string) {
defaultTab.value = tabName;
}
</script>
<style lang="less" scoped></style>

View File

@@ -221,6 +221,7 @@
<n-gi>
<n-form-item label="分配权限" path="permissions">
<n-input
:type="formParams.permissions.length > 30 ? 'textarea' : ''"
placeholder="请输入分配权限,多个权限用,分割"
v-model:value="formParams.permissions"
/>

View File

@@ -33,6 +33,7 @@
block-line
cascade
checkable
:default-expand-all="true"
:virtual-scroll="true"
:data="treeData"
:expandedKeys="expandedKeys"

View File

@@ -1,70 +0,0 @@
<template>
<n-card :bordered="false" class="proCard">
<div class="result-box">
<n-result status="error" title="操作失败" description="请核对并修改以下信息后,再重新提交。">
<div class="result-box-extra">
<p>您提交的内容有如下错误</p>
<p class="mt-3">
<n-space align="center">
<n-icon size="20" color="#f0a020">
<InfoCircleOutlined />
</n-icon>
<span>认证照片不够清晰</span>
<n-button type="info" text>立即修改</n-button>
</n-space>
</p>
<p class="mt-3">
<n-space>
<n-icon size="20" color="#f0a020">
<InfoCircleOutlined />
</n-icon>
<span>备注包含敏感字符并且不能包含政治相关</span>
<n-button type="info" text>立即修改</n-button>
</n-space>
</p>
</div>
<template #footer>
<div class="flex justify-center mb-4">
<n-space align="center">
<n-button type="info" @click="goHome">回到首页</n-button>
<n-button>查看详情</n-button>
<n-button>打印</n-button>
</n-space>
</div>
</template>
</n-result>
</div>
</n-card>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { useThemeVars } from 'naive-ui';
import { useRouter } from 'vue-router';
import { InfoCircleOutlined } from '@vicons/antd';
const router = useRouter();
const themeVars = useThemeVars();
const getTableHeaderColor = computed(() => {
return themeVars.value.tableHeaderColor;
});
function goHome() {
router.push('/');
}
</script>
<style lang="less" scoped>
.result-box {
width: 72%;
margin: 0 auto;
text-align: center;
padding-top: 5px;
&-extra {
padding: 24px 40px;
text-align: left;
background: v-bind(getTableHeaderColor);
border-radius: 4px;
}
}
</style>

View File

@@ -1,74 +0,0 @@
<template>
<n-card :bordered="false" class="proCard">
<div class="result-box">
<n-result
status="info"
title="提示"
description="本次提交将在24小时候内自动转入对方账户如操作失误请及时撤回"
>
<div class="result-box-extra">
<p>您提交的内容如下</p>
<p class="mt-3">
<n-space align="center">
<n-icon size="20" color="#18a058">
<CheckCircleOutlined />
</n-icon>
<span>转入支付宝账户189****54261980</span>
<n-button type="info" text>立即撤回</n-button>
</n-space>
</p>
<p class="mt-3">
<n-space>
<n-icon size="20" color="#18a058">
<CheckCircleOutlined />
</n-icon>
<span>转入支付宝账户187****54262980</span>
<n-button type="info" text>立即撤回</n-button>
</n-space>
</p>
</div>
<template #footer>
<div class="flex justify-center mb-4">
<n-space align="center">
<n-button type="info" @click="goHome">回到首页</n-button>
<n-button>查看详情</n-button>
<n-button>全部撤回</n-button>
</n-space>
</div>
</template>
</n-result>
</div>
</n-card>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { useThemeVars } from 'naive-ui';
import { useRouter } from 'vue-router';
import { CheckCircleOutlined } from '@vicons/antd';
const router = useRouter();
const themeVars = useThemeVars();
const getTableHeaderColor = computed(() => {
return themeVars.value.tableHeaderColor;
});
function goHome() {
router.push('/');
}
</script>
<style lang="less" scoped>
.result-box {
width: 72%;
margin: 0 auto;
text-align: center;
padding-top: 5px;
&-extra {
padding: 24px 40px;
text-align: left;
background: v-bind(getTableHeaderColor);
border-radius: 4px;
}
}
</style>

View File

@@ -1,55 +0,0 @@
<template>
<n-card :bordered="false" class="proCard">
<div class="result-box">
<n-result
status="success"
title="操作成功"
description="提交结果页用于反馈一系列操作任务的处理结果,如果仅是简单操作,灰色区域可以显示一些补充的信息。"
>
<div class="result-box-extra">
<p>已提交申请等待财务部门审核</p>
</div>
<template #footer>
<div class="flex justify-center mb-4">
<n-space align="center">
<n-button type="info" @click="goHome">回到首页</n-button>
<n-button>查看详情</n-button>
<n-button>打印</n-button>
</n-space>
</div>
</template>
</n-result>
</div>
</n-card>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { useRouter } from 'vue-router';
import { useThemeVars } from 'naive-ui';
const router = useRouter();
const themeVars = useThemeVars();
const getTableHeaderColor = computed(() => {
return themeVars.value.tableHeaderColor;
});
function goHome() {
router.push('/');
}
</script>
<style lang="less" scoped>
.result-box {
width: 72%;
margin: 0 auto;
text-align: center;
padding-top: 5px;
&-extra {
padding: 24px 40px;
text-align: left;
background: v-bind(getTableHeaderColor);
border-radius: 4px;
}
}
</style>

View File

@@ -22,6 +22,7 @@
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
:resizeHeightOffset="-10000"
>
<template #tableTitle>
<n-button type="primary" @click="addTable">

View File

@@ -0,0 +1,94 @@
<template>
<div>
<n-spin :show="show" description="请稍候...">
<n-form :label-width="100" :model="formValue" :rules="rules" ref="formRef">
<n-form-item label="申请提现开关" path="cashSwitch">
<n-radio-group v-model:value="formValue.cashSwitch" name="cashSwitch">
<n-space>
<n-radio :value="1">开启</n-radio>
<n-radio :value="2">关闭</n-radio>
</n-space>
</n-radio-group>
</n-form-item>
<n-form-item label="提现最低手续费(元)" path="cashMinFee">
<n-input placeholder="" v-model:value="formValue.cashMinFee" />
</n-form-item>
<n-form-item label="提现最低手续费比率" path="cashMinFeeRatio">
<n-input placeholder="" v-model:value="formValue.cashMinFeeRatio" />
</n-form-item>
<n-form-item label="提现最低金额" path="cashMinMoney">
<n-input placeholder="" v-model:value="formValue.cashMinMoney" />
</n-form-item>
<n-form-item label="提现提示信息" path="cashTips">
<Editor style="height: 320px" v-model:value="formValue.cashTips" />
</n-form-item>
<div>
<n-space>
<n-button type="primary" @click="formSubmit">保存更新</n-button>
</n-space>
</div>
</n-form>
</n-spin>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { useMessage } from 'naive-ui';
import { getConfig, updateConfig } from '@/api/sys/config';
import Editor from '@/components/Editor/editor.vue';
const group = ref('cash');
const show = ref(false);
const rules = {};
const formRef: any = ref(null);
const message = useMessage();
const formValue = ref({
cashSwitch: '',
cashMinFee: '',
cashMinFeeRatio: '',
cashMinMoney: 0,
cashTips: '',
});
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
updateConfig({ group: group.value, list: formValue.value })
.then((_res) => {
message.success('更新成功');
load();
})
.catch((error) => {
message.error(error.toString());
});
} else {
message.error('验证失败,请填写完整信息');
}
});
}
onMounted(() => {
load();
});
function load() {
show.value = true;
new Promise((_resolve, _reject) => {
getConfig({ group: group.value })
.then((res) => {
show.value = false;
formValue.value = res.list;
})
.catch((error) => {
show.value = false;
message.error(error.toString());
});
});
}
</script>

View File

@@ -0,0 +1,248 @@
<template>
<div>
<n-spin :show="show" description="请稍候...">
<n-form :label-width="100" :model="formValue" :rules="rules" ref="formRef">
<n-form-item label="开启debug" path="payDebug">
<n-switch size="large" v-model:value="formValue.payDebug" />
<template #feedback>开启后控制台会输出支付相关的日志</template>
</n-form-item>
<n-divider title-placement="left">支付宝</n-divider>
<n-alert :show-icon="false" type="info">
确保你已经申请开通过支付宝相关产品权限建议按照以下步骤进行配置
<br />1.
下载支付宝平台密钥工具下载地址https://opendocs.alipay.com/common/02kipk加签方式选择证书加密算法选择RSA2
<br />2. 生成后的私钥请在工具中转换为PKCS1格式 <br />3.
在支付宝中配置证书参考地址https://opendocs.alipay.com/common/02khjo?pathHash=5403bedd
</n-alert>
<n-form-item label="应用ID" path="payAliPayAppId">
<n-input v-model:value="formValue.payAliPayAppId" placeholder="" />
<template #feedback></template>
</n-form-item>
<n-form-item label="应用私钥路径" path="payAliPayPrivateKey">
<n-input v-model:value="formValue.payAliPayPrivateKey" placeholder="" clearable />
<template #feedback
>RSA2 加密算法默认生成格式为 PKCS8系统默认是RSA2加密切记转换为 PKCS1 格式</template
>
</n-form-item>
<n-form-item label="应用公钥" path="payAliPayAppCertPublicKey">
<n-input v-model:value="formValue.payAliPayAppCertPublicKey" placeholder="" clearable />
<template #feedback>appCertPublicKey.crt证书路径</template>
</n-form-item>
<n-form-item label="支付宝根证书路径" path="payAliPayRootCert">
<n-input v-model:value="formValue.payAliPayRootCert" placeholder="" clearable />
<template #feedback>alipayRootCert.crt证书路径"</template>
</n-form-item>
<n-form-item label="支付宝公钥证书路径" path="payAliPayCertPublicKeyRSA2">
<n-input v-model:value="formValue.payAliPayCertPublicKeyRSA2" placeholder="" clearable />
<template #feedback>alipayCertPublicKey_RSA2.crt证书路径"</template>
</n-form-item>
<n-divider title-placement="left">微信支付</n-divider>
<n-form-item label="应用ID" path="payWxPayAppId">
<n-input v-model:value="formValue.payWxPayAppId" placeholder="" />
<template #feedback>和微信配置中的微信公众号配置保持一致</template>
</n-form-item>
<n-form-item label="商户ID" path="payWxPayMchId">
<n-input v-model:value="formValue.payWxPayMchId" placeholder="" />
<template #feedback>商户ID 或者服务商模式的 sp_mchid</template>
</n-form-item>
<n-form-item label="证书序列号" path="payWxPaySerialNo">
<n-input v-model:value="formValue.payWxPaySerialNo" placeholder="" />
<template #feedback>商户证书的证书序列号</template>
</n-form-item>
<n-form-item label="APIv3Key" path="payWxPayAPIv3Key">
<n-input v-model:value="formValue.payWxPayAPIv3Key" placeholder="" clearable />
<template #feedback>商户平台获取</template>
</n-form-item>
<n-form-item label="私钥" path="payWxPayPrivateKey">
<n-input
type="textarea"
v-model:value="formValue.payWxPayPrivateKey"
placeholder=""
clearable
/>
<template #feedback>apiclient_key.pem 读取后的内容</template>
</n-form-item>
<n-divider title-placement="left">QQ支付</n-divider>
<n-form-item label="应用ID" path="payQQPayAppId">
<n-input v-model:value="formValue.payQQPayAppId" placeholder="" />
<template #feedback></template>
</n-form-item>
<n-form-item label="商户ID" path="payQQPayMchId">
<n-input v-model:value="formValue.payQQPayMchId" placeholder="" />
<template #feedback></template>
</n-form-item>
<n-form-item label="ApiKey" path="payQQPayApiKey">
<n-input
type="textarea"
v-model:value="formValue.payQQPayApiKey"
placeholder=""
clearable
/>
<template #feedback>API秘钥值</template>
</n-form-item>
<div>
<n-space>
<n-button type="primary" @click="formSubmit">保存更新</n-button>
<!-- <n-button type="default" @click="sendTest">测试支付</n-button>-->
</n-space>
</div>
</n-form>
</n-spin>
<n-modal
:block-scroll="false"
:mask-closable="false"
v-model:show="showModal"
:show-icon="false"
preset="dialog"
title="发送测试短信"
>
<n-form
:model="formParams"
:rules="rules"
ref="formTestRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-form-item label="事件模板" path="event">
<n-select :options="options.config_sms_template" v-model:value="formParams.event" />
</n-form-item>
<n-form-item label="手机号" path="mobile">
<n-input
placeholder="请输入接收手机号"
v-model:value="formParams.mobile"
:required="true"
/>
</n-form-item>
<n-form-item label="验证码" path="code">
<n-input
placeholder="请输入要接收的验证码"
v-model:value="formParams.code"
:required="true"
/>
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="() => (showModal = false)">关闭</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">发送</n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { useMessage } from 'naive-ui';
import { getConfig, sendTestSms, updateConfig } from '@/api/sys/config';
import { Dicts } from '@/api/dict/dict';
import { Options } from '@/utils/hotgo';
const group = ref('pay');
const show = ref(false);
const showModal = ref(false);
const formBtnLoading = ref(false);
const formParams = ref({ mobile: '', event: '', code: '1234' });
const rules = {};
const formTestRef = ref<any>();
const formRef: any = ref(null);
const message = useMessage();
const options = ref<Options>({
config_sms_template: [],
config_sms_drive: [],
});
const formValue = ref({
payDebug: true,
payAliPayAppId: '',
payAliPayPrivateKey: '',
payAliPayAppCertPublicKey: '',
payAliPayRootCert: '',
payAliPayCertPublicKeyRSA2: '',
payWxPayAppId: '',
payWxPayMchId: '',
payWxPaySerialNo: '',
payWxPayAPIv3Key: '',
payWxPayPrivateKey: '',
payQQPayAppId: '',
payQQPayMchId: '',
payQQPayApiKey: '',
});
function sendTest() {
showModal.value = true;
formBtnLoading.value = false;
}
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
updateConfig({ group: group.value, list: formValue.value }).then((_res) => {
message.success('更新成功');
load();
});
} else {
message.error('验证失败,请填写完整信息');
}
});
}
onMounted(() => {
load();
});
async function load() {
show.value = true;
await loadOptions();
new Promise((_resolve, _reject) => {
getConfig({ group: group.value })
.then((res) => {
formValue.value = res.list;
})
.finally(() => {
show.value = false;
});
});
}
async function loadOptions() {
options.value = await Dicts({
types: ['config_sms_template', 'config_sms_drive'],
});
}
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formTestRef.value.validate((errors) => {
if (!errors) {
sendTestSms(formParams.value).then((_res) => {
message.success('发送成功');
showModal.value = false;
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
</script>

View File

@@ -0,0 +1,121 @@
<template>
<div>
<n-spin :show="show" description="请稍候...">
<n-form :label-width="100" :model="formValue" :rules="rules" ref="formRef">
<n-divider title-placement="left">公众号</n-divider>
<n-form-item label="AppID" path="officialAccountAppId">
<n-input v-model:value="formValue.officialAccountAppId" placeholder="" />
<template #feedback>请填写微信公众平台后台的AppId</template>
</n-form-item>
<n-form-item label="AppSecret" path="officialAccountAppSecret">
<n-input v-model:value="formValue.officialAccountAppSecret" placeholder="" clearable />
<template #feedback>请填写微信公众平台后台的AppSecret</template>
</n-form-item>
<n-form-item label="Token" path="officialAccountToken">
<n-input v-model:value="formValue.officialAccountToken" placeholder="" clearable />
<template #feedback
>与公众平台接入设置值一致必须为英文或者数字长度为3到32个字符</template
>
</n-form-item>
<n-form-item label="EncodingAESKey" path="officialAccountEncodingAESKey">
<n-input
v-model:value="formValue.officialAccountEncodingAESKey"
placeholder=""
clearable
/>
<template #feedback
>与公众平台接入设置值一致必须为英文或者数字长度为43个字符 </template
>
</n-form-item>
<n-divider title-placement="left">开放平台</n-divider>
<n-form-item label="AppID" path="openPlatformAppId">
<n-input v-model:value="formValue.openPlatformAppId" placeholder="" />
<template #feedback>请填写微信开放平台后台的AppId</template>
</n-form-item>
<n-form-item label="AppSecret" path="openPlatformAppSecret">
<n-input v-model:value="formValue.openPlatformAppSecret" placeholder="" clearable />
<template #feedback>请填写微信开放平台后台的AppSecret</template>
</n-form-item>
<n-form-item label="Token" path="openPlatformToken">
<n-input v-model:value="formValue.openPlatformToken" placeholder="" clearable />
<template #feedback
>与开放平台接入设置值一致必须为英文或者数字长度为3到32个字符</template
>
</n-form-item>
<n-form-item label="EncodingAESKey" path="openPlatformEncodingAESKey">
<n-input v-model:value="formValue.openPlatformEncodingAESKey" placeholder="" clearable />
<template #feedback
>与开放平台接入设置值一致必须为英文或者数字长度为43个字符</template
>
</n-form-item>
<!-- <n-divider title-placement="left">小程序</n-divider>-->
<div>
<n-space>
<n-button type="primary" @click="formSubmit">保存更新</n-button>
</n-space>
</div>
</n-form>
</n-spin>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { useMessage } from 'naive-ui';
import { getConfig, updateConfig } from '@/api/sys/config';
const group = ref('wechat');
const show = ref(false);
const rules = {};
const formRef: any = ref(null);
const message = useMessage();
const formValue = ref({
officialAccountAppId: '',
officialAccountAppSecret: '',
officialAccountToken: '',
officialAccountEncodingAESKey: '',
openPlatformAppId: '',
openPlatformAppSecret: '',
openPlatformToken: '',
openPlatformEncodingAESKey: '',
});
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
updateConfig({ group: group.value, list: formValue.value }).then((_res) => {
message.success('更新成功');
load();
});
} else {
message.error('验证失败,请填写完整信息');
}
});
}
onMounted(() => {
load();
});
async function load() {
show.value = true;
new Promise((_resolve, _reject) => {
getConfig({ group: group.value })
.then((res) => {
formValue.value = res.list;
})
.finally(() => {
show.value = false;
});
});
}
</script>

View File

@@ -22,8 +22,11 @@
<RevealSetting v-if="type === 3" />
<EmailSetting v-if="type === 4" />
<SmsSetting v-if="type === 5" />
<CashSetting v-if="type === 7" />
<UploadSetting v-if="type === 8" />
<GeoSetting v-if="type === 9" />
<PaySetting v-if="type === 10" />
<WechatSetting v-if="type === 11" />
</n-card>
</n-grid-item>
</n-grid>
@@ -35,20 +38,23 @@
import RevealSetting from './RevealSetting.vue';
import EmailSetting from './EmailSetting.vue';
import ThemeSetting from './ThemeSetting.vue';
import CashSetting from './CashSetting.vue';
import UploadSetting from './UploadSetting.vue';
import GeoSetting from './GeoSetting.vue';
import SmsSetting from './SmsSetting.vue';
import PaySetting from './PaySetting.vue';
import WechatSetting from './WechatSetting.vue';
const typeTabList = [
{
name: '基本设置',
desc: '系统常规设置',
key: 1,
},
{
name: '主题设置',
desc: '系统主题设置',
key: 2,
},
// {
// name: '主题设置',
// desc: '系统主题设置',
// key: 2,
// },
// {
// name: '显示设置',
// desc: '系统显示设置',
@@ -65,15 +71,15 @@
key: 5,
},
// {
// name: '下游配置',
// name: '管理员配置',
// desc: '默认设置和权限屏蔽',
// key: 6,
// },
// {
// name: '提现配置',
// desc: '提现规则配置',
// key: 7,
// },
{
name: '提现配置',
desc: '管理员提现规则配置',
key: 7,
},
{
name: '云存储',
desc: '配置上传文件驱动',
@@ -84,6 +90,16 @@
desc: '配置地理位置工具',
key: 9,
},
{
name: '支付配置',
desc: '支付宝/微信/QQ支付配置等',
key: 10,
},
{
name: '微信配置',
desc: '公众号/开放平台/小程序配置等',
key: 11,
},
];
export default defineComponent({
components: {
@@ -91,9 +107,12 @@
RevealSetting,
EmailSetting,
ThemeSetting,
CashSetting,
UploadSetting,
GeoSetting,
SmsSetting,
PaySetting,
WechatSetting,
},
setup() {
const state = reactive({

View File

@@ -83,7 +83,7 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
},
},
brotliSize: false,
chunkSizeWarningLimit: 2000,
chunkSizeWarningLimit: 3000,
},
};
};