Compare commits

..

10 Commits

1
.gitignore vendored

@ -10,6 +10,7 @@ lerna-debug.log*
node_modules node_modules
.DS_Store .DS_Store
dist dist
dist.zip
dist-ssr dist-ssr
coverage coverage
*.local *.local

@ -22,6 +22,7 @@ declare module 'vue' {
ElForm: typeof import('element-plus/es')['ElForm'] ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem'] ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElHeader: typeof import('element-plus/es')['ElHeader'] ElHeader: typeof import('element-plus/es')['ElHeader']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput'] ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElMain: typeof import('element-plus/es')['ElMain'] ElMain: typeof import('element-plus/es')['ElMain']
@ -35,6 +36,7 @@ declare module 'vue' {
ElSwitch: typeof import('element-plus/es')['ElSwitch'] ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable'] ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTag: typeof import('element-plus/es')['ElTag']
ElTree: typeof import('element-plus/es')['ElTree'] ElTree: typeof import('element-plus/es')['ElTree']
MapSelector: typeof import('./components/MapSelector.vue')['default'] MapSelector: typeof import('./components/MapSelector.vue')['default']
Pagination: typeof import('./components/Pagination/index.vue')['default'] Pagination: typeof import('./components/Pagination/index.vue')['default']

@ -4,12 +4,13 @@
<el-scrollbar> <el-scrollbar>
<el-menu <el-menu
:default-active="route.path" :default-active="route.path"
:default-openeds="openKeys"
active-text-color="#1890FF" active-text-color="#1890FF"
background-color="#fff" background-color="#fff"
text-color="#000" text-color="#000"
router router
> >
<template v-for="(item, index) in routes" :key="index"> <template v-for="(item, index) in routes" :key="item.path">
<el-sub-menu <el-sub-menu
:index="'/' + item.path" :index="'/' + item.path"
v-if="item.children && item.children.filter((el) => !el.meta.hide).length > 0" v-if="item.children && item.children.filter((el) => !el.meta.hide).length > 0"
@ -20,7 +21,7 @@
<el-menu-item <el-menu-item
v-for="(ele, idx) in item.children.filter((el) => !el.meta.hide)" v-for="(ele, idx) in item.children.filter((el) => !el.meta.hide)"
:index="'/' + item.path + '/' + ele.path" :index="'/' + item.path + '/' + ele.path"
:key="idx" :key="ele.path"
> >
{{ ele.meta.title }}</el-menu-item {{ ele.meta.title }}</el-menu-item
> >
@ -42,6 +43,17 @@ const userStore = useUserStore()
const route = useRoute() const route = useRoute()
const routes = userStore.routerList const routes = userStore.routerList
const openKeys = ref([])
watch(
() => route.path,
(path) => {
const parent = '/' + path.split('/')[1]
openKeys.value = [parent]
},
{ immediate: true },
)
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

@ -18,6 +18,12 @@ export const constantRoutes = [
// 动态路由配置(需要权限) // 动态路由配置(需要权限)
export const asyncRoutes = [ export const asyncRoutes = [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard/index.vue'),
meta: { title: '仪表盘', permissions: ['dashboard'] },
},
{ {
path: 'SystemManage', path: 'SystemManage',
meta: { title: '系统管理', permissions: ['system'] }, meta: { title: '系统管理', permissions: ['system'] },
@ -50,12 +56,24 @@ export const asyncRoutes = [
component: () => import('@/views/ModelManage/index.vue'), component: () => import('@/views/ModelManage/index.vue'),
meta: { title: '模版管理', permissions: ['template'] }, meta: { title: '模版管理', permissions: ['template'] },
}, },
{
path: 'ModelApproval',
name: 'ModelApproval',
component: () => import('@/views/ModelApproval/index.vue'),
meta: { title: '模版审批', permissions: ['template_approval'] },
},
{ {
path: 'ComboManage', path: 'ComboManage',
name: 'ComboManage', name: 'ComboManage',
component: () => import('@/views/ComboManage/index.vue'), component: () => import('@/views/ComboManage/index.vue'),
meta: { title: '套餐管理', permissions: ['membership_plan'] }, meta: { title: '套餐管理', permissions: ['membership_plan'] },
}, },
{
path: 'VendorManage',
name: 'VendorManage',
component: () => import('@/views/VendorManage/index.vue'),
meta: { title: '厂商管理', permissions: ['vendor_manage'] },
},
{ {
path: 'GenerateRecord', path: 'GenerateRecord',
name: 'GenerateRecord', name: 'GenerateRecord',
@ -68,6 +86,12 @@ export const asyncRoutes = [
component: () => import('@/views/OrderManage/index.vue'), component: () => import('@/views/OrderManage/index.vue'),
meta: { title: '订单管理', permissions: ['order_management'] }, meta: { title: '订单管理', permissions: ['order_management'] },
}, },
{
path: 'RequirementManage',
name: 'RequirementManage',
component: () => import('@/views/RequirementManage/index.vue'),
meta: { title: '需求管理', permissions: ['requirement_manage'] },
},
{ {
path: 'NoPermission', path: 'NoPermission',
name: 'NoPermission', name: 'NoPermission',

@ -34,3 +34,23 @@ body {
.pointer { .pointer {
cursor: pointer; cursor: pointer;
} }
*::-webkit-scrollbar {
width: 12px;
height: 12px;
}
*::-webkit-scrollbar-button {
width: 0px;
height: 0px;
display: none;
}
*::-webkit-scrollbar-corner {
background-color: transparent;
}
*::-webkit-scrollbar-thumb {
border: 4px solid rgba(0, 0, 0, 0);
height: 6px;
border-radius: 25px;
background-clip: padding-box;
background-color: rgba(0, 0, 0, 0.3);
}

@ -46,7 +46,6 @@ instance.interceptors.request.use(
// HTTP response拦截 // HTTP response拦截
instance.interceptors.response.use( instance.interceptors.response.use(
(response) => { (response) => {
console.log(response)
if (response.data.error.code === 200) { if (response.data.error.code === 200) {
return Promise.resolve(response.data.result) return Promise.resolve(response.data.result)
} else if (response.data.error.code === 401) { } else if (response.data.error.code === 401) {

@ -81,6 +81,9 @@ if (!isAdd.value) {
<el-form-item prop="code" label="字段"> <el-form-item prop="code" label="字段">
<el-input placeholder="请输入" v-model="formField.code" /> <el-input placeholder="请输入" v-model="formField.code" />
</el-form-item> </el-form-item>
<el-form-item prop="desc" label="描述">
<el-input placeholder="请输入" v-model="formField.desc" />
</el-form-item>
<el-form-item prop="duration_days" label="持续时间(天)"> <el-form-item prop="duration_days" label="持续时间(天)">
<el-input-number placeholder="请输入" v-model="formField.duration_days" /> <el-input-number placeholder="请输入" v-model="formField.duration_days" />
</el-form-item> </el-form-item>

@ -36,6 +36,7 @@
> >
<el-table-column align="center" label="名称" prop="name" show-overflow-tooltip /> <el-table-column align="center" label="名称" prop="name" show-overflow-tooltip />
<el-table-column align="center" label="字段" prop="code" show-overflow-tooltip /> <el-table-column align="center" label="字段" prop="code" show-overflow-tooltip />
<el-table-column align="center" label="描述" prop="desc" show-overflow-tooltip />
<el-table-column <el-table-column
align="center" align="center"
label="持续时间(天)" label="持续时间(天)"

@ -0,0 +1,128 @@
<template>
<div class="order-type-chart">
<div class="chart-header">
<h3>订单类型分布</h3>
</div>
<v-chart class="chart" :option="chartOption" autoresize />
<!-- <div class="legend">
<div class="legend-item" v-for="item in legendData" :key="item.name">
<span class="legend-color" :style="{ backgroundColor: item.color }"></span>
<span class="legend-text">{{ item.name }}</span>
</div>
</div> -->
</div>
</template>
<script setup>
import { ref } from 'vue'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { PieChart } from 'echarts/charts'
import { TitleComponent, TooltipComponent, LegendComponent } from 'echarts/components'
import VChart from 'vue-echarts'
import RPC from '@/utils/rpc'
use([CanvasRenderer, PieChart, TitleComponent, TooltipComponent, LegendComponent])
const legendData = ref([
{ name: '充值', color: '#409eff' },
{ name: '包月', color: '#67c23a' },
{ name: '包季', color: '#e6a23c' },
{ name: '包年', color: '#f56c6c' },
])
RPC.post('/dashboard/order_type_distribution').then((res) => {
console.log('order_type_distribution', res)
chartData.value = res.items.map((item) => {
return {
name: item.type,
value: item.value_cents / 100,
}
})
})
const chartData = ref([
// { value: 35, name: '', itemStyle: { color: '#409eff' } },
// { value: 25, name: '', itemStyle: { color: '#67c23a' } },
// { value: 25, name: '', itemStyle: { color: '#e6a23c' } },
// { value: 15, name: '', itemStyle: { color: '#f56c6c' } },
])
const chartOption = ref({
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c}% ({d}%)',
},
legend: {
bottom: '0%',
left: 'center',
},
series: [
{
name: '订单类型',
type: 'pie',
radius: ['50%', '80%'],
center: ['50%', '50%'],
avoidLabelOverlap: false,
label: {
show: false,
},
emphasis: {
label: {
show: false,
},
},
labelLine: {
show: false,
},
data: chartData,
},
],
})
</script>
<style lang="scss" scoped>
.order-type-chart {
padding: 20px;
.chart-header {
margin-bottom: 20px;
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #303133;
}
}
.chart {
height: 300px;
width: 100%;
}
.legend {
display: flex;
justify-content: center;
gap: 24px;
margin-top: 20px;
.legend-item {
display: flex;
align-items: center;
gap: 8px;
.legend-color {
width: 12px;
height: 12px;
border-radius: 50%;
}
.legend-text {
font-size: 14px;
color: #606266;
}
}
}
}
</style>

@ -0,0 +1,145 @@
<template>
<div class="recent-drawings">
<div class="card-header">
<h3>最近图纸生成</h3>
<el-button type="primary" size="small" plain @click="goAll"></el-button>
</div>
<div class="drawings-list">
<div v-for="item in drawingData" :key="item.id" class="drawing-item">
<div class="drawing-info">
<div class="drawing-title">{{ item.file_name }}</div>
<div class="drawing-details">
<span class="drawing-id">生成编号{{ item.order_no }}</span>
<span class="drawing-user">用户{{ item.username }}({{ item.user_id }})</span>
</div>
<div class="drawing-time">
生成时间{{
item.CreatedAt ? dayjs(item.CreatedAt).format('YYYY-MM-DD HH:mm:ss') : ''
}}
</div>
</div>
<div class="drawing-status">
<el-tag :type="getStatusTagType(item.status)" size="small">
<template v-if="item.status == 0"></template>
<template v-if="item.status == 1"></template>
<template v-if="item.status == 2"></template>
<template v-if="item.status == 3">()</template>
<template v-if="item.status == 4"></template>
<template v-if="item.status == 5"></template>
</el-tag>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import RPC from '@/utils/rpc'
import dayjs from 'dayjs'
const router = useRouter()
const goAll = () => router.push('/GenerateRecord')
const loading = ref(false)
const getList = () => {
loading.value = true
RPC.post('/planned_list', { page: 1, page_size: 10 })
.then((res) => {
drawingData.value = res.list || []
})
.finally(() => {
loading.value = false
})
}
getList()
const drawingData = ref([])
const getStatusTagType = (status) => {
// const statusMap = {
// : 'success',
// : 'warning',
// : 'danger',
// : 'info',
// }
const statusMap = ['info', 'info', 'info', 'warning', 'success', 'danger']
return statusMap[status] || ''
}
</script>
<style lang="scss" scoped>
.recent-drawings {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
height: 100%;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #303133;
}
}
.drawings-list {
height: 280px;
overflow-y: auto;
.drawing-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid #f0f2f5;
&:last-child {
border-bottom: none;
}
&:hover {
background-color: #f8f9fa;
margin: 0 -12px;
padding: 16px 12px;
border-radius: 6px;
}
.drawing-info {
flex: 1;
.drawing-title {
font-size: 14px;
font-weight: 500;
color: #303133;
margin-bottom: 6px;
}
.drawing-details {
display: flex;
gap: 16px;
margin-bottom: 4px;
.drawing-id,
.drawing-user {
font-size: 12px;
color: #909399;
}
}
.drawing-time {
font-size: 12px;
color: #c0c4cc;
}
}
.drawing-status {
flex-shrink: 0;
}
}
}
}
</style>

@ -0,0 +1,120 @@
<template>
<div class="recent-orders">
<div class="card-header">
<h3>最近订单</h3>
<el-button type="primary" size="small" plain @click="goAll"></el-button>
</div>
<div class="table-container">
<el-table :data="orderData" style="width: 100%" size="small" height="270">
<el-table-column align="center" prop="order_no" label="订单编号" show-overflow-tooltip />
<el-table-column align="center" prop="user_name" label="用户" show-overflow-tooltip />
<el-table-column align="center" prop="amount" label="金额" width="100">
<template #default="scope">
<span class="amount">{{ scope.row.amount }}</span>
</template>
</el-table-column>
<el-table-column align="center" prop="order_type" label="类型" width="100">
</el-table-column>
<el-table-column align="center" prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag v-if="row.status === 'created'" type="warning" size="small"></el-tag>
<el-tag v-if="row.status === 'pending'" type="warning" size="small"></el-tag>
<el-tag v-if="row.status === 'paid'" type="success" size="small"></el-tag>
<el-tag v-if="row.status === 'failed'" type="danger" size="small"></el-tag>
<el-tag v-if="row.status === 'canceled'" type="info" size="small"></el-tag>
<el-tag v-if="row.status === 'refunded'" type="info" size="small">退</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import RPC from '@/utils/rpc'
import dayjs from 'dayjs'
const router = useRouter()
const goAll = () => router.push('/OrderManage')
const orderData = ref([])
const loading = ref(false)
const getList = () => {
loading.value = true
RPC.post('/membership/order/list', { page: 1, size: 10 })
.then((res) => {
orderData.value = res.list
})
.finally(() => {
loading.value = false
})
}
getList()
const getTypeTagType = (type) => {
const typeMap = {
包年: '',
包季: 'success',
充值: 'info',
包月: 'warning',
}
return typeMap[type] || ''
}
const getStatusTagType = (status) => {
const statusMap = {
已支付: 'success',
待支付: 'warning',
已取消: 'danger',
}
return statusMap[status] || ''
}
</script>
<style lang="scss" scoped>
.recent-orders {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
height: 100%;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #303133;
}
}
.table-container {
.amount {
font-weight: 600;
color: #303133;
}
}
}
:deep(.el-table) {
.el-table__header {
th {
background-color: #fafafa;
color: #606266;
font-weight: 500;
}
}
.el-table__row {
&:hover {
background-color: #f5f7fa;
}
}
}
</style>

@ -0,0 +1,181 @@
<template>
<div class="revenue-chart">
<div class="chart-header">
<h3>收入趋势</h3>
<div class="chart-tabs">
<el-button
size="small"
:class="{ active: activeTab === 'month' }"
@click="switchTab('month')"
>
月度
</el-button>
<el-button
size="small"
:class="{ active: activeTab === 'quarter' }"
@click="switchTab('quarter')"
>
季度
</el-button>
<el-button
size="small"
:class="{ active: activeTab === 'year' }"
@click="switchTab('year')"
>
年度
</el-button>
</div>
</div>
<v-chart class="chart" :option="chartOption" autoresize />
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { LineChart } from 'echarts/charts'
import {
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent,
} from 'echarts/components'
import VChart from 'vue-echarts'
import RPC from '@/utils/rpc'
use([CanvasRenderer, LineChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent])
const activeTab = ref('month')
const switchTab = (tab) => {
activeTab.value = tab
console.log(activeTab.value)
getData()
}
const data = ref([])
const getData = () => {
RPC.post('/dashboard/revenue_trend', { granularity: activeTab.value }).then((res) => {
console.log(activeTab.value, res)
data.value = res.items || []
})
}
getData()
const chartOption = computed(() => {
return {
tooltip: {
trigger: 'axis',
formatter: '{b}: ¥{c}',
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'category',
boundaryGap: false,
data: data.value.map((item) => item.label),
axisLine: {
lineStyle: {
color: '#e4e7ed',
},
},
axisLabel: {
color: '#606266',
},
},
yAxis: {
type: 'value',
axisLine: {
show: false,
},
axisTick: {
show: false,
},
axisLabel: {
color: '#606266',
formatter: '{value}',
},
splitLine: {
lineStyle: {
color: '#f5f7fa',
},
},
},
series: [
{
type: 'line',
data: data.value.map((item) => item.value_cents / 100),
smooth: true,
lineStyle: {
color: '#409eff',
width: 3,
},
itemStyle: {
color: '#409eff',
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: 'rgba(64, 158, 255, 0.3)',
},
{
offset: 1,
color: 'rgba(64, 158, 255, 0.1)',
},
],
},
},
},
],
}
})
</script>
<style lang="scss" scoped>
.revenue-chart {
padding: 20px;
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #303133;
}
.chart-tabs {
.el-button {
margin-left: 8px;
&.active {
background-color: #157ee8;
border-color: #177de3;
color: white;
}
}
}
}
.chart {
height: 300px;
width: 100%;
}
}
</style>

@ -0,0 +1,295 @@
<template>
<div class="dashboard">
<!-- 统计卡片 -->
<div class="stats-cards">
<div class="stat-card">
<div class="stat-content">
<div class="stat-info">
<div class="stat-label">总用户数</div>
<div class="stat-value">{{ data.total_users }}</div>
<div
class="stat-trend"
:class="{
positive: data.total_users_mom_ratio?.includes('+'),
negative:
data.total_users_mom_ratio?.includes('-') &&
data.total_users_mom_ratio.length > 1,
}"
>
<el-icon v-if="data.total_users_mom_ratio?.includes('+')"><ArrowUp /></el-icon>
<el-icon
v-if="
data.total_users_mom_ratio?.includes('-') && data.total_users_mom_ratio.length > 1
"
><ArrowDown
/></el-icon>
{{ data.total_users_mom_ratio }} 较上月
</div>
</div>
<div class="stat-icon user">
<el-icon><User /></el-icon>
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-content">
<div class="stat-info">
<div class="stat-label">今日活跃</div>
<div class="stat-value">{{ data.active_today }}</div>
<div
class="stat-trend"
:class="{
positive: data.active_today_dod_ratio?.includes('+'),
negative:
data.active_today_dod_ratio?.includes('-') &&
data.active_today_dod_ratio.length > 1,
}"
>
<el-icon v-if="data.active_today_dod_ratio?.includes('+')"><ArrowUp /></el-icon>
<el-icon
v-if="
data.active_today_dod_ratio?.includes('-') &&
data.active_today_dod_ratio.length > 1
"
><ArrowDown
/></el-icon>
{{ data.active_today_dod_ratio }} 较昨日
</div>
</div>
<div class="stat-icon active">
<el-icon><TrendCharts /></el-icon>
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-content">
<div class="stat-info">
<div class="stat-label">商品生成量</div>
<div class="stat-value">{{ data.drawings_generated }}</div>
<div
class="stat-trend"
:class="{
positive: data.drawings_generated_mom_ratio?.includes('+'),
negative:
data.drawings_generated_mom_ratio?.includes('-') &&
data.drawings_generated_mom_ratio?.length > 1,
}"
>
<el-icon v-if="data.drawings_generated_mom_ratio?.includes('+')"><ArrowUp /></el-icon>
<el-icon
v-if="
data.drawings_generated_mom_ratio?.includes('-') &&
data.drawings_generated_mom_ratio?.length > 1
"
><ArrowDown
/></el-icon>
{{ data.drawings_generated_mom_ratio }} 较上月
</div>
</div>
<div class="stat-icon generate">
<el-icon><Document /></el-icon>
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-content">
<div class="stat-info">
<div class="stat-label">本月收入</div>
<div class="stat-value">¥{{ data.month_income_cents / 100 }}</div>
<div
class="stat-trend"
:class="{
positive: data.month_income_cents_mom_ratio?.includes('+'),
negative:
data.month_income_cents_mom_ratio?.includes('-') &&
data.month_income_cents_mom_ratio?.length > 1,
}"
>
<el-icon v-if="data.month_income_cents_mom_ratio?.includes('+')"><ArrowUp /></el-icon>
<el-icon
v-if="
data.month_income_cents_mom_ratio?.includes('-') &&
data.month_income_cents_mom_ratio?.length > 1
"
><ArrowDown
/></el-icon>
{{ data.month_income_cents_mom_ratio }} 较上月
</div>
</div>
<div class="stat-icon revenue">
<el-icon><Money /></el-icon>
</div>
</div>
</div>
</div>
<!-- 图表区域 -->
<div class="charts-section">
<div class="chart-container">
<RevenueChart />
</div>
<div class="chart-container">
<OrderTypeChart />
</div>
</div>
<!-- 最近数据区域 -->
<div class="recent-section">
<div class="recent-container">
<RecentOrders />
</div>
<div class="recent-container">
<RecentDrawings />
</div>
</div>
</div>
</template>
<script setup>
import { ArrowUp, ArrowDown, User, TrendCharts, Document, Money } from '@element-plus/icons-vue'
import RevenueChart from './RevenueChart.vue'
import OrderTypeChart from './OrderTypeChart.vue'
import RecentOrders from './RecentOrders.vue'
import RecentDrawings from './RecentDrawings.vue'
import RPC from '@/utils/rpc'
const data = ref({})
const getData = () => {
RPC.post('/dashboard/summary').then((res) => {
data.value = res
})
}
getData()
</script>
<style lang="scss" scoped>
.dashboard {
padding: 20px;
// background-color: #f5f7fa;
min-height: 100vh;
.stats-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
margin-bottom: 30px;
.stat-card {
background: white;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease;
&:hover {
transform: translateY(-2px);
}
.stat-content {
display: flex;
justify-content: space-between;
align-items: center;
.stat-info {
.stat-label {
font-size: 14px;
color: #909399;
margin-bottom: 8px;
}
.stat-value {
font-size: 28px;
font-weight: 600;
color: #303133;
margin-bottom: 8px;
}
.stat-trend {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
&.positive {
color: #67c23a;
}
&.negative {
color: #f56c6c;
}
.el-icon {
font-size: 12px;
}
}
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
.el-icon {
font-size: 24px;
color: white;
}
&.user {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
&.active {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
&.generate {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
&.revenue {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
}
}
}
}
}
.charts-section {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
margin-bottom: 30px;
@media (max-width: 1200px) {
grid-template-columns: 1fr;
}
.chart-container {
min-height: 400px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
}
.recent-section {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
@media (max-width: 1200px) {
grid-template-columns: 1fr;
}
.recent-container {
height: 300px;
}
}
}
</style>

@ -34,7 +34,22 @@
v-loading="loading" v-loading="loading"
header-row-class-name="table-header" header-row-class-name="table-header"
> >
<el-table-column
align="center"
label="订单号"
prop="order_no"
width="200"
show-overflow-tooltip
/>
<el-table-column align="center" label="文件名" prop="file_name" show-overflow-tooltip /> <el-table-column align="center" label="文件名" prop="file_name" show-overflow-tooltip />
<el-table-column
align="center"
label="用户名"
prop="username"
width="100"
show-overflow-tooltip
/>
<el-table-column align="center" label="用户ID" prop="user_id" width="100" />
<el-table-column align="center" label="状态" prop="status" width="100"> <el-table-column align="center" label="状态" prop="status" width="100">
<template #default="{ row }"> <template #default="{ row }">
<span v-if="row.status == 0"></span> <span v-if="row.status == 0"></span>
@ -47,7 +62,7 @@
</el-table-column> </el-table-column>
<el-table-column align="center" label="创建时间" prop="CreatedAt" width="220"> <el-table-column align="center" label="创建时间" prop="CreatedAt" width="220">
<template #default="{ row }"> <template #default="{ row }">
{{ row.CreatedAt ? dayjs(row.CreatedAt).format('YYYY-DD-MM HH:mm:ss') : '' }} {{ row.CreatedAt ? dayjs(row.CreatedAt).format('YYYY-MM-DD HH:mm:ss') : '' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column align="center" label="操作" width="120"> <el-table-column align="center" label="操作" width="120">

@ -0,0 +1,134 @@
<script setup>
import RPC from '@/utils/rpc'
const props = defineProps({
type: {
type: String,
required: true,
},
row: [Object],
templateList: [Array],
})
const emit = defineEmits(['closed', 'onSuccess'])
const dialogVisible = ref(false)
const isCancel = computed(() => props.type === 'cancel')
const isApprove = computed(() => props.type === 'approve')
const isReject = computed(() => props.type === 'reject')
const isApproval = computed(() => isApprove.value || isReject.value)
onMounted(() => {
dialogVisible.value = true
})
const formRef = ref(null)
const formField = ref({ remark: '' })
const formRules = reactive({
remark: [{ required: true, message: '备注不可为空', trigger: 'change' }],
})
const loading = ref(false)
function closed() {
emit('closed')
}
const submitForm = async () => {
await formRef.value.validate()
if (isApproval.value) {
//
const params = {
application_id: props.row.id,
action: isApprove.value ? 'approve' : 'reject',
remark: formField.value.remark,
}
loading.value = true
RPC.post('/template_access_review', params)
.then((res) => {
ElMessage.success('操作成功')
emit('onSuccess')
emit('closed')
})
.finally(() => {
loading.value = false
})
} else {
const params = {
template_id: props.row.template_id,
user_id: props.row.user_id,
remark: formField.value.remark,
}
loading.value = true
RPC.post('/template_access_revoke', params)
.then((res) => {
ElMessage.success('操作成功')
emit('onSuccess')
emit('closed')
})
.finally(() => {
loading.value = false
})
}
}
</script>
<template>
<el-dialog
align-center
:title="isApprove ? '同意申请' : isReject ? '拒绝申请' : isCancel ? '撤回权限' : ''"
:width="isApproval ? '500px' : '80%'"
v-model="dialogVisible"
@closed="closed"
:close-on-click-modal="false"
>
<el-form
ref="formRef"
label-position="right"
label-suffix=":"
:model="formField"
:rules="formRules"
v-loading="loading"
label-width="150"
>
<!-- 审批表单 -->
<template v-if="isApproval">
<el-form-item label="申请用户">
<span>{{ row?.nickname }}</span>
</el-form-item>
<el-form-item label="申请原因">
<span>{{ row?.reason }}</span>
</el-form-item>
<el-form-item prop="remark" label="备注">
<el-input type="textarea" :rows="4" placeholder="请输入备注" v-model="formField.remark" />
</el-form-item>
</template>
<template v-else="isApproval">
<el-form-item label="撤回用户">
<span>{{ row?.nickname || row?.user_id }}</span>
</el-form-item>
<el-form-item label="撤回模板">
<span>
{{
templateList?.find((item) => item.id === row.template_id)?.name || row.template_id
}}</span
>
</el-form-item>
<el-form-item prop="remark" label="备注">
<el-input type="textarea" :rows="4" placeholder="请输入备注" v-model="formField.remark" />
</el-form-item>
</template>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="closed"> </el-button>
<el-button :type="isReject ? 'danger' : 'primary'" :loading="loading" @click="submitForm">
{{ isApprove ? '同意' : isReject ? '拒绝' : '确定' }}
</el-button>
</div>
</template>
</el-dialog>
</template>
<style lang="scss" scoped></style>

@ -0,0 +1,175 @@
<template>
<div>
<div class="title">模板审批</div>
<el-form
ref="searchFormRef"
class="form"
:inline="true"
label-width="98px"
:model="queryParams"
>
<el-form-item label="申请用户" prop="nickname">
<el-input
clearable
placeholder="请输入"
style="width: 220px"
v-model="queryParams.nickname"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label-width="100px">
<el-button icon="Search" type="primary" @click="handleQuery"> </el-button>
<el-button icon="RefreshRight" @click="resetQuery"> </el-button>
</el-form-item>
</el-form>
<div style="margin-bottom: 20px">
<!-- <el-button icon="Plus" type="primary" @click="add"> </el-button> -->
</div>
<div ref="tableContainerEl" class="table_border table">
<el-table
ref="tableRef"
:data="tableData"
v-loading="loading"
header-row-class-name="table-header"
>
<el-table-column align="center" label="申请用户" prop="nickname" show-overflow-tooltip>
<template #default="{ row }">
{{ row.nickname || row.user_id }}
</template>
</el-table-column>
<el-table-column align="center" label="申请原因" prop="reason" show-overflow-tooltip />
<el-table-column align="center" label="申请模板" prop="template_id" show-overflow-tooltip>
<template #default="{ row }">
{{ templateList.find((item) => item.id === row.template_id)?.name || row.template_id }}
</template>
</el-table-column>
<el-table-column align="center" label="状态" prop="status">
<template #default="{ row }">
<span v-if="row.status === 'pending'"></span>
<span v-if="row.status === 'rejected'"></span>
<span v-if="row.status === 'approved'"></span>
<span v-if="row.status === 'revoked'"></span>
</template>
</el-table-column>
<el-table-column align="center" label="备注" prop="remark" show-overflow-tooltip />
<el-table-column align="center" label="操作" width="120">
<template #default="{ row }">
<el-button
v-if="row.status === 'pending'"
link
type="primary"
@click="handleApproval(row, 'approve')"
>
同意
</el-button>
<el-button
v-if="row.status === 'approved'"
link
type="danger"
@click="handleApproval(row, 'cancel')"
>
撤回权限
</el-button>
<el-button
v-if="row.status === 'pending'"
link
type="danger"
@click="handleApproval(row, 'reject')"
>
拒绝
</el-button>
<span v-if="row.status !== 'pending' && row.status !== 'approved'">--</span>
</template>
</el-table-column>
</el-table>
</div>
<pagination
v-show="total > 0"
:total="total"
v-model:limit="queryParams.limit"
v-model:page="queryParams.page"
@pagination="getList"
/>
<HandleDialog
v-if="isShowDialog"
:type="rowType"
:row="currentRow"
@closed="isShowDialog = false"
@on-success="getList"
:templateList="templateList"
/>
</div>
</template>
<script setup>
import { useTableRPC } from '@/utils/hook'
import HandleDialog from './HandleDialog.vue'
import RPC from '@/utils/rpc.js'
const rowType = ref('')
const currentRow = ref(null)
const add = () => {
isShowDialog.value = true
currentRow.value = null
rowType.value = 'add'
}
const edit = (row) => {
isShowDialog.value = true
rowType.value = 'edit'
currentRow.value = row
}
const templateList = ref([])
RPC.post('/template/list', { page: 1, limit: -1 }).then((res) => {
templateList.value = res.rows || []
})
const {
loading,
tableData,
tableRef,
searchFormRef,
total,
queryParams,
getList,
handleQuery,
resetQuery,
handleDelete,
isShowDialog,
maxHeight,
tableContainerEl,
} = useTableRPC({
queryParams: {
orderby: [{ sort: 'asc' }],
},
remoteDataFn: '/template/access/list',
// deleteConfig: {
// remote: '/vendor/delete',
// key: 'id',
// message(row) {
// return ` ${row.name}`
// },
// },
})
const handleApproval = (row, action) => {
isShowDialog.value = true
currentRow.value = row
rowType.value = action
}
getList()
</script>
<style lang="scss" scoped>
.title {
font-size: 20px;
font-weight: 700;
margin-bottom: 20px;
}
</style>

@ -64,6 +64,7 @@ const addRow = () => {
type: 'string', type: 'string',
excel_loc: '', excel_loc: '',
enabled: true, enabled: true,
required: true,
}) })
} else { } else {
formField.value.params = [ formField.value.params = [
@ -74,6 +75,7 @@ const addRow = () => {
type: 'string', type: 'string',
excel_loc: '', excel_loc: '',
enabled: true, enabled: true,
required: true,
}, },
] ]
} }
@ -87,7 +89,7 @@ const delRow = (index) => {
<el-dialog <el-dialog
align-center align-center
:title="isAdd ? '新增' : '修改'" :title="isAdd ? '新增' : '修改'"
width="80%" width="90%"
v-model="dialogVisible" v-model="dialogVisible"
@closed="closed" @closed="closed"
:close-on-click-modal="false" :close-on-click-modal="false"
@ -107,6 +109,9 @@ const delRow = (index) => {
<el-form-item prop="code" label="字段"> <el-form-item prop="code" label="字段">
<el-input placeholder="请输入" v-model="formField.code" /> <el-input placeholder="请输入" v-model="formField.code" />
</el-form-item> </el-form-item>
<el-form-item prop="price_cents" label="价格(分)">
<el-input-number placeholder="请输入" v-model="formField.price_cents" />
</el-form-item>
<el-form-item prop="sheet_name" label="表格名称"> <el-form-item prop="sheet_name" label="表格名称">
<el-input placeholder="请输入" v-model="formField.sheet_name" /> <el-input placeholder="请输入" v-model="formField.sheet_name" />
</el-form-item> </el-form-item>
@ -128,6 +133,11 @@ const delRow = (index) => {
<el-input placeholder="请输入" v-model="row.name" /> <el-input placeholder="请输入" v-model="row.name" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column align="center" label="字段分类">
<template #default="{ row }">
<el-input placeholder="请输入" v-model="row.category" />
</template>
</el-table-column>
<!-- <el-table-column align="center" label="字段键"> <!-- <el-table-column align="center" label="字段键">
<template #default="{ row }"> <template #default="{ row }">
<el-input placeholder="请输入" v-model="row.key" /> <el-input placeholder="请输入" v-model="row.key" />
@ -138,14 +148,30 @@ const delRow = (index) => {
<el-input placeholder="请输入" v-model="row.value" /> <el-input placeholder="请输入" v-model="row.value" />
</template> </template>
</el-table-column> </el-table-column>
<!-- <el-table-column align="center" label="字段类型"> <el-table-column align="center" label="字段类型">
<template #default="{ row }"> <template #default="{ row }">
<el-select placeholder="请输入" v-model="row.type"> <el-select placeholder="请输入" v-model="row.type">
<el-option value="string" label="字符串"></el-option> <el-option value="string" label="字符串"></el-option>
<el-option value="number" label="数字"></el-option> <el-option value="number" label="数字"></el-option>
<el-option value="select" label="下拉框"></el-option>
</el-select> </el-select>
</template> </template>
</el-table-column> --> </el-table-column>
<el-table-column align="center" label="选项">
<template #default="{ row }">
<el-input placeholder="以,分隔每个选项" v-model="row.options" />
</template>
</el-table-column>
<el-table-column align="center" label="最小值">
<template #default="{ row }">
<el-input-number controls-position="right" placeholder="请输入" v-model="row.min" />
</template>
</el-table-column>
<el-table-column align="center" label="最大值">
<template #default="{ row }">
<el-input-number controls-position="right" placeholder="请输入" v-model="row.max" />
</template>
</el-table-column>
<el-table-column align="center" label="字段位置"> <el-table-column align="center" label="字段位置">
<template #default="{ row }"> <template #default="{ row }">
<el-input placeholder="请输入" v-model="row.excel_loc" /> <el-input placeholder="请输入" v-model="row.excel_loc" />
@ -156,6 +182,11 @@ const delRow = (index) => {
<el-switch v-model="row.enabled" active-text="" inactive-text="" /> <el-switch v-model="row.enabled" active-text="" inactive-text="" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column align="center" label="是否必填">
<template #default="{ row }">
<el-switch v-model="row.required" active-text="" inactive-text="" />
</template>
</el-table-column>
<el-table-column align="center" label="操作" width="60"> <el-table-column align="center" label="操作" width="60">
<template #default="{ $index }"> <template #default="{ $index }">
<el-button link type="danger" @click="delRow($index)"> </el-button> <el-button link type="danger" @click="delRow($index)"> </el-button>

@ -35,6 +35,11 @@
header-row-class-name="table-header" header-row-class-name="table-header"
> >
<el-table-column align="center" label="名称" prop="name" show-overflow-tooltip /> <el-table-column align="center" label="名称" prop="name" show-overflow-tooltip />
<el-table-column align="center" label="价格(¥)" prop="price_cents">
<template #default="{ row }">
{{ row.price_cents ? row.price_cents / 100 : '' }}
</template>
</el-table-column>
<el-table-column align="center" label="字段" prop="code" show-overflow-tooltip /> <el-table-column align="center" label="字段" prop="code" show-overflow-tooltip />
<el-table-column align="center" label="表格名称" prop="sheet_name" show-overflow-tooltip /> <el-table-column align="center" label="表格名称" prop="sheet_name" show-overflow-tooltip />
<el-table-column align="center" label="路径" prop="auto_path" show-overflow-tooltip /> <el-table-column align="center" label="路径" prop="auto_path" show-overflow-tooltip />
@ -46,6 +51,7 @@
</el-table-column> </el-table-column>
<el-table-column align="center" label="操作" width="120"> <el-table-column align="center" label="操作" width="120">
<template #default="{ row }"> <template #default="{ row }">
<el-button link type="primary" @click="copy(row)"> </el-button>
<el-button link type="primary" @click="edit(row)"> </el-button> <el-button link type="primary" @click="edit(row)"> </el-button>
<el-button link type="danger" @click="handleDelete(row)"> </el-button> <el-button link type="danger" @click="handleDelete(row)"> </el-button>
@ -75,6 +81,7 @@
<script setup> <script setup>
import { useTableRPC } from '@/utils/hook' import { useTableRPC } from '@/utils/hook'
import HandleDialog from './HandleDialog.vue' import HandleDialog from './HandleDialog.vue'
import RPC from '@/utils/rpc'
const rowType = ref('') const rowType = ref('')
const currentRow = ref(null) const currentRow = ref(null)
@ -89,6 +96,17 @@ const edit = (row) => {
currentRow.value = row currentRow.value = row
} }
const copy = (row) => {
ElMessageBox.confirm(`确认复制${row.name}?`, '提醒', {
type: 'warning',
}).then(() => {
RPC.post('/template/copy', { template_id: row.id }).then(() => {
ElMessage.success('复制成功')
handleQuery()
})
})
}
const { const {
loading, loading,
tableData, tableData,
@ -104,7 +122,9 @@ const {
maxHeight, maxHeight,
tableContainerEl, tableContainerEl,
} = useTableRPC({ } = useTableRPC({
queryParams: {}, queryParams: {
orderby: [{ id: 'desc' }],
},
remoteDataFn: '/template/list', remoteDataFn: '/template/list',
deleteConfig: { deleteConfig: {
remote: '/template/delete', remote: '/template/delete',

@ -64,7 +64,7 @@
</el-table-column> </el-table-column>
<el-table-column align="center" label="创建时间" prop="CreatedAt" width="220"> <el-table-column align="center" label="创建时间" prop="CreatedAt" width="220">
<template #default="{ row }"> <template #default="{ row }">
{{ row.created_at ? dayjs(row.created_at * 1000).format('YYYY-DD-MM HH:mm:ss') : '' }} {{ row.created_at ? dayjs(row.created_at * 1000).format('YYYY-MM-DD HH:mm:ss') : '' }}
</template> </template>
</el-table-column> </el-table-column>
<!-- <el-table-column align="center" label="操作" width="120"> <!-- <el-table-column align="center" label="操作" width="120">

@ -0,0 +1,136 @@
<script setup>
import RPC from '@/utils/rpc'
const props = defineProps({
type: {
type: String,
required: true,
},
row: [Object],
})
const emit = defineEmits(['closed', 'onSuccess'])
const dialogVisible = ref(false)
const isAdd = computed(() => props.type === 'add')
const isApprove = computed(() => props.type === 'approve')
const isReject = computed(() => props.type === 'reject')
const isApproval = computed(() => isApprove.value || isReject.value)
onMounted(() => {
dialogVisible.value = true
})
const formRef = ref(null)
const formField = ref({ enabled: true, params: [], remark: '' })
const formRules = reactive({
name: [{ required: true, message: '不可为空', trigger: 'change' }],
code: [{ required: true, message: '不可为空', trigger: 'change' }],
remark: [{ required: true, message: '备注不可为空', trigger: 'change' }],
})
const loading = ref(false)
function closed() {
emit('closed')
}
const submitForm = async () => {
await formRef.value.validate()
if (isApproval.value) {
//
const params = {
application_id: props.row.id,
action: isApprove.value ? 'approve' : 'reject',
remark: formField.value.remark
}
loading.value = true
RPC.post('/template_access_review', params)
.then((res) => {
ElMessage.success('操作成功')
emit('onSuccess')
emit('closed')
})
.finally(() => {
loading.value = false
})
} else {
//
const api = isAdd.value ? '/vendor/create' : '/vendor/update'
const params = {
...formField.value,
id: isAdd.value ? null : props.row.id,
}
loading.value = true
RPC.post(api, params)
.then((res) => {
ElMessage.success('操作成功')
emit('onSuccess')
emit('closed')
})
.finally(() => {
loading.value = false
})
}
}
if (!isAdd.value && !isApproval.value) {
formField.value = {
...props.row,
}
}
</script>
<template>
<el-dialog
align-center
:title="isApprove ? '同意申请' : isReject ? '拒绝申请' : isAdd ? '新增' : '修改'"
:width="isApproval ? '500px' : '80%'"
v-model="dialogVisible"
@closed="closed"
:close-on-click-modal="false"
>
<el-form
ref="formRef"
label-position="right"
label-suffix=":"
:model="formField"
:rules="formRules"
v-loading="loading"
label-width="150"
>
<!-- 审批表单 -->
<template v-if="isApproval">
<el-form-item label="申请用户">
<span>{{ row?.nickname }}</span>
</el-form-item>
<el-form-item label="申请原因">
<span>{{ row?.reason }}</span>
</el-form-item>
<el-form-item prop="remark" label="备注">
<el-input
type="textarea"
:rows="4"
placeholder="请输入备注"
v-model="formField.remark"
/>
</el-form-item>
</template>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="closed"> </el-button>
<el-button
:type="isReject ? 'danger' : 'primary'"
:loading="loading"
@click="submitForm"
>
{{ isApprove ? '同意' : isReject ? '拒绝' : '确定' }}
</el-button>
</div>
</template>
</el-dialog>
</template>
<style lang="scss" scoped></style>

@ -0,0 +1,133 @@
<template>
<div>
<div class="title">需求管理</div>
<el-form
ref="searchFormRef"
class="form"
:inline="true"
label-width="98px"
:model="queryParams"
>
<el-form-item label="名称" prop="name">
<el-input
clearable
placeholder="请输入"
style="width: 220px"
v-model="queryParams.name"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="分类" prop="category">
<el-input
clearable
placeholder="请输入"
style="width: 220px"
v-model="queryParams.category"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label-width="100px">
<el-button icon="Search" type="primary" @click="handleQuery"> </el-button>
<el-button icon="RefreshRight" @click="resetQuery"> </el-button>
</el-form-item>
</el-form>
<div style="margin-bottom: 20px">
<!-- <el-button icon="Plus" type="primary" @click="add"> </el-button> -->
</div>
<div ref="tableContainerEl" class="table_border table">
<el-table
ref="tableRef"
:data="tableData"
v-loading="loading"
header-row-class-name="table-header"
>
<el-table-column align="center" label="分类" prop="category" show-overflow-tooltip />
<el-table-column align="center" label="名称" prop="name" show-overflow-tooltip />
<el-table-column align="center" label="描述" prop="description" show-overflow-tooltip />
<el-table-column align="center" label="提出用户" prop="proposer" show-overflow-tooltip />
<el-table-column align="center" label="邮箱" prop="email" show-overflow-tooltip />
<el-table-column align="center" label="手机" prop="phone" show-overflow-tooltip />
<el-table-column
align="center"
label="联系厂商"
prop="vendor_contact"
show-overflow-tooltip
/>
<el-table-column
align="center"
label="模板名称"
prop="template_name"
show-overflow-tooltip
/>
<el-table-column align="center" label="操作" width="120">
<template #default="{ row }">
<el-button link type="danger" @click="handleDelete(row)"> </el-button>
</template>
</el-table-column>
</el-table>
</div>
<pagination
v-show="total > 0"
:total="total"
v-model:limit="queryParams.limit"
v-model:page="queryParams.page"
@pagination="getList"
/>
<HandleDialog
v-if="isShowDialog"
:type="rowType"
:row="currentRow"
@closed="isShowDialog = false"
@on-success="getList"
/>
</div>
</template>
<script setup>
import { useTableRPC } from '@/utils/hook'
import HandleDialog from './HandleDialog.vue'
import RPC from '@/utils/rpc.js'
const {
loading,
tableData,
tableRef,
searchFormRef,
total,
queryParams,
getList,
handleQuery,
resetQuery,
handleDelete,
isShowDialog,
maxHeight,
tableContainerEl,
} = useTableRPC({
queryParams: {
orderby: [{ id: 'desc' }],
},
remoteDataFn: '/requirement/list',
deleteConfig: {
remote: '/requirement/delete',
key: 'id',
message(row) {
return `确认删除 “${row.name}`
},
},
})
getList()
</script>
<style lang="scss" scoped>
.title {
font-size: 20px;
font-weight: 700;
margin-bottom: 20px;
}
</style>

@ -85,9 +85,9 @@ if (!isAdd.value) {
<el-form-item v-if="isAdd" prop="password" label="密码"> <el-form-item v-if="isAdd" prop="password" label="密码">
<el-input placeholder="请输入" v-model="formField.password" /> <el-input placeholder="请输入" v-model="formField.password" />
</el-form-item> </el-form-item>
<el-form-item prop="phone" label="电话"> <!-- <el-form-item prop="phone" label="电话">
<el-input placeholder="请输入" v-model="formField.phone" /> <el-input placeholder="请输入" v-model="formField.phone" />
</el-form-item> </el-form-item> -->
<el-form-item prop="email" label="邮箱"> <el-form-item prop="email" label="邮箱">
<el-input placeholder="请输入" v-model="formField.email" /> <el-input placeholder="请输入" v-model="formField.email" />
</el-form-item> </el-form-item>

@ -42,12 +42,38 @@
{{ row.roles?.map((item) => item.name)?.join(',') }} {{ row.roles?.map((item) => item.name)?.join(',') }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column align="center" label="厂商" prop="vendors">
<template #default="{ row }">
{{ row.vendors?.map((item) => item.name)?.join(',') }}
</template>
</el-table-column>
<el-table-column align="center" label="操作" width="180"> <el-table-column align="center" label="操作" width="180">
<template #default="{ row }"> <template #default="{ row }">
<div class="flex items-center">
<el-button link type="primary" @click="editRole(row)"> </el-button> <el-button link type="primary" @click="editRole(row)"> </el-button>
<el-button link type="primary" @click="edit(row)"> </el-button> <el-button link type="primary" @click="edit(row)"> </el-button>
<el-button link type="danger" @click="handleDelete(row)"> </el-button> <el-button link type="danger" @click="handleDelete(row)"> </el-button>
</div>
<template v-if="useUserStore().userInfo?.isAdmin">
<div class="flex items-center">
<el-button link type="primary" @click="order(row)"> </el-button>
<el-button link type="primary" @click="pay(row)"> </el-button>
</div>
<div class="flex items-center">
<el-button link type="primary" @click="vendor(row, 'authorize')">
分配厂商
</el-button>
<el-button
link
type="primary"
v-if="row.vendors?.length > 0"
@click="vendor(row, 'revoke')"
>
撤销厂商
</el-button>
</div>
</template>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -75,6 +101,65 @@
@closed="isShowRoleDialog = false" @closed="isShowRoleDialog = false"
@on-success="getList" @on-success="getList"
/> />
<el-dialog
align-center
title="充值"
width="500"
v-model="isShowPayDialog"
@closed="payClosed"
:close-on-click-modal="false"
>
是否给{{ currentRow.username }}进行充值
<el-input-number v-model="pay_amount"></el-input-number>
<template #footer>
<div class="dialog-footer">
<el-button @click="payClosed"> </el-button>
<el-button type="primary" :loading="payLoading" @click="payConfirm"> </el-button>
</div>
</template>
</el-dialog>
<el-dialog
align-center
title="赠送套餐"
width="500"
v-model="isShowOrderDialog"
@closed="orderClosed"
:close-on-click-modal="false"
>
是否给{{ currentRow.username }}进行赠送
<el-select v-model="order_id">
<el-option v-for="item in planList" :value="item.id" :label="item.name"></el-option>
</el-select>
<template #footer>
<div class="dialog-footer">
<el-button @click="orderClosed"> </el-button>
<el-button type="primary" :loading="orderLoading" @click="orderConfirm">
</el-button>
</div>
</template>
</el-dialog>
<el-dialog
align-center
:title="currentType === 'authorize' ? '分配厂商' : '撤销厂商'"
width="500"
v-model="isShowVendorDialog"
@closed="vendorClosed"
:close-on-click-modal="false"
>
<el-checkbox-group v-model="vendorSelected">
<el-checkbox v-for="item in vendorListFilter" :label="item.name" :value="item.id" />
</el-checkbox-group>
<template #footer>
<div class="dialog-footer">
<el-button @click="vendorClosed"> </el-button>
<el-button type="primary" :loading="vendorLoading" @click="vendorConfirm">
</el-button>
</div>
</template>
</el-dialog>
</div> </div>
</template> </template>
@ -84,6 +169,7 @@ import dayjs from 'dayjs'
import RPC from '@/utils/rpc' import RPC from '@/utils/rpc'
import HandleDialog from './HandleDialog.vue' import HandleDialog from './HandleDialog.vue'
import RoleDialog from './RoleDialog.vue' import RoleDialog from './RoleDialog.vue'
import { useUserStore } from '@/stores/user'
const rowType = ref('') const rowType = ref('')
const currentRow = ref(null) const currentRow = ref(null)
@ -108,6 +194,137 @@ RPC.post('/role/list', { page: 1, limit: -1 }).then((res) => {
roleList.value = res.rows || [] roleList.value = res.rows || []
}) })
const pay_amount = ref(0)
const isShowPayDialog = ref(false)
const payLoading = ref(false)
const payClosed = () => {
isShowPayDialog.value = false
currentRow.value = {}
}
const pay = (row) => {
currentRow.value = row
isShowPayDialog.value = true
pay_amount.value = 0
}
const payConfirm = () => {
if (!pay_amount.value) {
ElMessage.warning('请填写充值金额')
return
}
payLoading.value = true
RPC.post('/wallet/admin/recharge_and_pay', {
user_id: currentRow.value.id,
amount_cents: pay_amount.value * 100,
pay_channel: 'admin',
remark: '手动充值',
})
.then(() => {
isShowPayDialog.value = false
ElMessage.success('充值成功')
getList()
})
.finally(() => {
payLoading.value = false
})
}
const planList = ref([])
RPC.post('/membership_plan/list', { page: 0, limit: -1, orderby: [{ sort: 'asc' }] }).then(
(res) => {
planList.value = res.rows || []
},
)
const order_id = ref(0)
const isShowOrderDialog = ref(false)
const orderLoading = ref(false)
const orderClosed = () => {
isShowOrderDialog.value = false
currentRow.value = {}
order_id.value = null
}
const order = (row) => {
currentRow.value = row
isShowOrderDialog.value = true
order_id.value = null
}
const orderConfirm = () => {
if (!order_id) {
ElMessage.warning('请选择赠送套餐')
return
}
orderLoading.value = true
RPC.post('/membership/admin/order/create_and_pay', {
user_id: currentRow.value.id,
plan_id: order_id.value,
pay_channel: 'admin',
remark: '管理员手动完成',
})
.then(() => {
isShowOrderDialog.value = false
ElMessage.success('赠送成功')
getList()
})
.finally(() => {
orderLoading.value = false
})
}
const isShowVendorDialog = ref(false)
const vendorLoading = ref(false)
const vendorClosed = () => {
isShowVendorDialog.value = false
currentRow.value = {}
vendorSelected.value = []
}
const currentType = ref('')
const vendor = (row, type) => {
if (type === 'authorize') {
vendorListFilter.value = vendorList.value
vendorSelected.value = row.vendors.map((item) => item.vendor_id)
}
if (type === 'revoke') {
vendorListFilter.value = row.vendors.map((item) => {
return {
id: item.vendor_id,
...item,
}
})
}
currentType.value = type
currentRow.value = row
isShowVendorDialog.value = true
}
const vendorConfirm = () => {
if (vendorSelected?.value.length <= 0) {
ElMessage.warning('请选择厂商')
return
}
vendorLoading.value = true
RPC.post(`/vendor_user_${currentType.value}`, {
user_id: currentRow.value.id,
vendor_ids: vendorSelected.value,
})
.then(() => {
isShowVendorDialog.value = false
ElMessage.success('操作成功')
getList()
})
.finally(() => {
vendorLoading.value = false
})
}
const vendorList = ref([])
const vendorListFilter = ref([])
const vendorSelected = ref([])
RPC.post('/vendor/list', { page: 1, limit: -1 }).then((res) => {
vendorList.value = res.rows || []
})
const { const {
loading, loading,
tableData, tableData,

@ -0,0 +1,103 @@
<script setup>
import RPC from '@/utils/rpc'
const props = defineProps({
type: {
type: String,
required: true,
},
row: [Object],
})
const emit = defineEmits(['closed', 'onSuccess'])
const dialogVisible = ref(false)
const isAdd = computed(() => props.type === 'add')
onMounted(() => {
dialogVisible.value = true
})
const formRef = ref(null)
const formField = ref({ enabled: true })
const formRules = reactive({
name: [{ required: true, message: '不可为空', trigger: 'change' }],
code: [{ required: true, message: '不可为空', trigger: 'change' }],
})
const loading = ref(false)
function closed() {
emit('closed')
}
const submitForm = async () => {
await formRef.value.validate()
const api = isAdd.value ? '/vendor/create' : '/vendor/update'
const params = {
...formField.value,
id: isAdd.value ? null : props.row.id,
}
loading.value = true
RPC.post(api, params)
.then((res) => {
ElMessage.success('操作成功')
emit('onSuccess')
emit('closed')
})
.finally(() => {
loading.value = false
})
}
if (!isAdd.value) {
formField.value = {
...props.row,
}
}
</script>
<template>
<el-dialog
align-center
:title="isAdd ? '新增' : '修改'"
width="80%"
v-model="dialogVisible"
@closed="closed"
:close-on-click-modal="false"
>
<el-form
ref="formRef"
label-position="right"
label-suffix=":"
:model="formField"
:rules="formRules"
v-loading="loading"
label-width="150"
>
<el-form-item prop="name" label="名称">
<el-input placeholder="请输入" v-model="formField.name" />
</el-form-item>
<el-form-item prop="code" label="字段">
<el-input placeholder="请输入" v-model="formField.code" />
</el-form-item>
<el-form-item prop="contact_name" label="联系人">
<el-input placeholder="请输入" v-model="formField.contact_name" />
</el-form-item>
<el-form-item prop="address" label="地址">
<el-input placeholder="请输入" v-model="formField.address" />
</el-form-item>
<el-form-item prop="enabled" label="是否启用">
<el-switch v-model="formField.enabled" active-text="" inactive-text="" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="closed"> </el-button>
<el-button type="primary" :loading="loading" @click="submitForm"> </el-button>
</div>
</template>
</el-dialog>
</template>
<style lang="scss" scoped></style>

@ -0,0 +1,128 @@
<template>
<div>
<div class="title">厂商管理</div>
<el-form
ref="searchFormRef"
class="form"
:inline="true"
label-width="98px"
:model="queryParams"
>
<el-form-item label="名称" prop="name">
<el-input
clearable
placeholder="请输入"
style="width: 220px"
v-model="queryParams.name"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label-width="100px">
<el-button icon="Search" type="primary" @click="handleQuery"> </el-button>
<el-button icon="RefreshRight" @click="resetQuery"> </el-button>
</el-form-item>
</el-form>
<div style="margin-bottom: 20px">
<el-button icon="Plus" type="primary" @click="add"> </el-button>
</div>
<div ref="tableContainerEl" class="table_border table">
<el-table
ref="tableRef"
:data="tableData"
v-loading="loading"
header-row-class-name="table-header"
>
<el-table-column align="center" label="名称" prop="name" show-overflow-tooltip />
<el-table-column align="center" label="字段" prop="code" show-overflow-tooltip />
<el-table-column align="center" label="联系人" prop="contact_name" show-overflow-tooltip />
<el-table-column align="center" label="地址" prop="address" show-overflow-tooltip />
<el-table-column align="center" label="是否启用" prop="enabled">
<template #default="{ row }">
{{ row.enabled ? '启用' : '禁用' }}
</template>
</el-table-column>
<el-table-column align="center" label="操作" width="120">
<template #default="{ row }">
<el-button link type="primary" @click="edit(row)"> </el-button>
<el-button link type="danger" @click="handleDelete(row)"> </el-button>
</template>
</el-table-column>
</el-table>
</div>
<pagination
v-show="total > 0"
:total="total"
v-model:limit="queryParams.limit"
v-model:page="queryParams.page"
@pagination="getList"
/>
<HandleDialog
v-if="isShowDialog"
:type="rowType"
:row="currentRow"
@closed="isShowDialog = false"
@on-success="getList"
/>
</div>
</template>
<script setup>
import { useTableRPC } from '@/utils/hook'
import HandleDialog from './HandleDialog.vue'
const rowType = ref('')
const currentRow = ref(null)
const add = () => {
isShowDialog.value = true
currentRow.value = null
rowType.value = 'add'
}
const edit = (row) => {
isShowDialog.value = true
rowType.value = 'edit'
currentRow.value = row
}
const {
loading,
tableData,
tableRef,
searchFormRef,
total,
queryParams,
getList,
handleQuery,
resetQuery,
handleDelete,
isShowDialog,
maxHeight,
tableContainerEl,
} = useTableRPC({
queryParams: {
orderby: [{ id: 'desc' }],
},
remoteDataFn: '/vendor/list',
deleteConfig: {
remote: '/vendor/delete',
key: 'id',
message(row) {
return `确认删除 “${row.name}`
},
},
})
getList()
</script>
<style lang="scss" scoped>
.title {
font-size: 20px;
font-weight: 700;
margin-bottom: 20px;
}
</style>
Loading…
Cancel
Save