@@ -188,4 +188,109 @@ export default {
data,
}),
},
+
+ // 文件管理
+ file: {
+ // 文件列表
+ list: (params) =>
+ request({
+ url: 'admin/file',
+ method: 'GET',
+ params,
+ }),
+ // 文件详情
+ detail: (id) =>
+ url: `admin/file/${id}`,
+ // 文件上传
+ upload: (params, data) =>
+ url: 'admin/file/upload',
+ method: 'POST',
+ data,
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ // 删除文件
+ delete: (id) =>
+ method: 'DELETE',
+ options: {
+ showSuccessMessage: true,
+ // 批量删除文件
+ batchDelete: (ids) =>
+ url: 'admin/file/batchDelete',
+ data: { ids },
+ // 移动文件到分组
+ move: (data) =>
+ url: 'admin/file/move',
+ // 重命名文件
+ rename: (id, data) =>
+ url: `admin/file/rename/${id}`,
+ method: 'PUT',
+ // 文件分组管理
+ group: {
+ // 分组列表
+ list: () =>
+ url: 'admin/file/group',
+ // 添加分组
+ add: (data) =>
+ // 编辑分组
+ edit: (id, data) =>
+ url: `admin/file/group/${id}`,
+ // 删除分组
};
@@ -1,37 +0,0 @@
-<template>
- <el-container>
- <el-main>
- <el-form :model="modal.params.detail" label-position="left" label-width="100px">
- <el-form-item label="标题"> 内容 </el-form-item>
- <el-form-item label="ID">
- {{ modal.params.detail.id }}
- </el-form-item>
- <el-form-item label="管理员昵称">
- {{ modal.params.detail.nickname }}
- <el-form-item label="链接">
- {{ modal.params.detail.url }}
- <el-form-item label="描述">
- {{ modal.params.detail.description }}
- <el-form-item label="操作内容">
- {{ modal.params.detail.params }}
- <el-form-item label="IP">
- {{ modal.params.detail.ip }}
- <el-form-item label="user agent">
- {{ modal.params.detail.useragent }}
- <el-form-item label="创建时间">
- {{ modal.params.detail.create_time }}
- </el-form>
- </el-main>
- </el-container>
-</template>
-
-<script setup>
- const props = defineProps(['modal']);
-</script>
@@ -1,230 +0,0 @@
- <el-container class="adminlog-view panel-block">
- <el-header class="sa-header">
- <div class="sa-title sa-flex sa-row-between">
- <div class="label sa-flex">
- <span class="left">操作纪录</span>
- <search-condition
- :conditionLabel="filterParams.conditionLabel"
- @deleteFilter="deleteFilter"
- ></search-condition>
- </div>
- <div>
- <el-button class="sa-button-refresh" icon="RefreshRight" @click="getData()"></el-button>
- <el-button class="sa-button-refresh" icon="Search" @click="openFilter"></el-button>
- </el-header>
- <el-main class="sa-p-0" v-loading="loading">
- <el-table class="sa-table" height="100%" :data="table.data" @row-dblclick="detailRow" stripe>
- <template #empty>
- <sa-empty />
- </template>
- <el-table-column prop="id" label="ID" min-width="100"> </el-table-column>
- <el-table-column label="访问地址" min-width="160">
- <template #default="scope">
- <span class="sa-table-line-1">
- {{ scope.row.url || '-' }}
- </span>
- </el-table-column>
- <el-table-column label="描述" min-width="260">
- <div class="sa-table-line-1">{{ scope.row.description }}</div>
- <el-table-column label="请求参数" min-width="260">
- <div class="sa-flex">
- <el-input v-model="scope.row.params" disabled>
- <template #append>
- <span class="cursor-pointer" @click="useClip(scope.row.params)">复制</span>
- </el-input>
- <el-table-column label="IP" min-width="140">
- <div class="table-ip">
- {{ scope.row.ip || '-' }}
- <el-table-column label="管理员" min-width="160">
- <sa-user-profile :user="scope.row.admin" :id="scope.row.admin_id" />
- <el-table-column label="创建时间" min-width="172">
- {{ scope.row.create_time || '-' }}
- <el-table-column fixed="right" label="操作">
- <el-button class="is-link" type="primary" @click="detailRow(scope.row)">详情</el-button>
- </el-table>
- <sa-view-bar>
- <template #right>
- <sa-pagination :pageData="pageData" @updateFn="getData" />
- </sa-view-bar>
-<script>
- export default {
- name: 'admin.auth.adminlog',
- };
- import { onMounted, reactive, ref } from 'vue';
- import admin from '@/app/admin/api';
- import { useModal, usePagination } from '@/sheep/hooks';
- import { useSearch } from '@/sheep/components/sa-table/sa-search/useSearch';
- import useClip from '@/sheep/utils/clipboard.js';
- import AdminLogDetail from './detail.vue';
- import { composeFilter } from '@/sheep/utils';
- import { cloneDeep } from 'lodash';
- const filterParams = reactive({
- tools: {
- keyword: {
- type: 'tinputprepend',
- placeholder: '请输入查询内容',
- field: 'keyword',
- field: 'id',
- value: '',
- },
- options: [
- {
- label: 'ID',
- value: 'id',
- label: '访问地址',
- value: 'url',
- label: '描述',
- value: 'description',
- ],
- admin: {
- field: 'admin',
- field: 'admin_id',
- label: '管理员ID',
- value: 'admin_id',
- label: '账号',
- value: 'admin.account',
- label: '昵称',
- value: 'admin.nickname',
- label: '手机号',
- value: 'admin.mobile',
- create_time: {
- type: 'tdatetimerange',
- label: '更新时间',
- field: 'create_time',
- value: [],
- data: {
- keyword: { field: 'id', value: '' },
- admin: { field: 'admin_id', value: '' },
- create_time: [],
- conditionLabel: {},
- });
- const { openFilter, deleteFilter } = useSearch({
- filterParams,
- getData,
- const { pageData } = usePagination();
- // 列表
- const table = reactive({
- data: [],
- order: '',
- sort: '',
- const loading = ref(true);
- // 获取
- async function getData(page) {
- loading.value = true;
- if (page) pageData.page = page;
- let tempSearch = cloneDeep(filterParams.data);
- let search = composeFilter(tempSearch, {
- url: 'like',
- description: 'like',
- 'admin.account': 'like',
- 'admin.nickname': 'like',
- 'admin.mobile': 'like',
- create_time: 'range',
- const { error, data } = await admin.auth.adminLog.list({
- page: pageData.page,
- list_rows: pageData.list_rows,
- ...search,
- order: table.order,
- sort: table.sort,
- if (error === 0) {
- table.data = data.data;
- pageData.page = data.current_page;
- pageData.list_rows = data.per_page;
- pageData.total = data.total;
- }
- loading.value = false;
- function detailRow(row) {
- useModal(
- AdminLogDetail,
- title: '详情',
- type: 'detail',
- detail: row,
- confirm: () => {
- getData();
- );
- onMounted(() => {
-<style lang="scss" scoped>
- .adminlog-view {
- .table-ip {
- color: var(--el-color-primary);
-</style>
@@ -1,297 +0,0 @@
- <el-container class="panel-block">
- <el-tabs class="sa-tabs" v-model="configType" @tab-change="getData">
- <el-tab-pane v-for="c in configList" :key="c" :label="c.label" :name="c.api"></el-tab-pane>
- </el-tabs>
- <el-main class="sa-p-t-30">
- <el-row :gutter="16">
- <el-col :xs="24" :sm="20" :md="18" :lg="14" :xl="14">
- <template v-if="configType == 'admin.config.basic.site'">
- <el-form
- :model="state.site.model"
- :rules="state.site.rules"
- ref="siteRef"
- label-width="180px"
- >
- <el-form-item label="Logo" prop="logo">
- <sa-uploader v-model="state.site.model.logo" fileType="image"></sa-uploader>
- <el-form-item label="站点名称" prop="name">
- <el-input placeholder="请输入站点名称" v-model="state.site.model.name"></el-input>
- <el-form-item label="版本号" prop="version">
- <el-input placeholder="请输入版本号" v-model="state.site.model.version"></el-input>
- <el-form-item label="版权信息">
- <el-row :gutter="16" class="version-copy">
- <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
- <el-form-item prop="copyright">
- <el-input
- placeholder="请输入版权信息"
- v-model="state.site.model.copyright"
- ></el-input>
- </el-col>
- <el-form-item prop="copytime">
- v-model="state.site.model.copytime"
- </el-row>
- <!-- <el-form-item label="全站Cdn地址" prop="cdnurl">
- placeholder="请输入全站Cdn地址"
- v-model="state.site.model.cdnurl"
- </el-form-item> -->
- <el-form-item label="备案号" prop="beian">
- <el-input placeholder="请输入备案号" v-model="state.site.model.beian"></el-input>
- <el-form-item label="工信部网址" prop="beian_url">
- placeholder="请输入工信部网址"
- v-model="state.site.model.beian_url"
- <template v-if="configType == 'admin.config.basic.login'">
- :model="state.login.model"
- :rules="state.login.rules"
- ref="loginRef"
- <el-form-item label="登录背景" prop="bg">
- <sa-uploader v-model="state.login.model.bg" fileType="image"></sa-uploader>
- <el-form-item label="登录验证码" prop="captcha" required>
- <el-radio-group v-model="state.login.model.captcha">
- <el-radio label="none">关闭验证码</el-radio>
- <el-radio label="code">图片验证码</el-radio>
- <!-- <el-radio label="geetest">极验验证码</el-radio> -->
- </el-radio-group>
- <template v-if="state.login.model.captcha == 'code'">
- <div class="sa-title--line sa-m-b-30">图片验证码</div>
- <el-form-item label="算数验证码" prop="captcha_code.math" required>
- <el-switch
- v-model="state.login.model.captcha_code.math"
- :active-value="1"
- :inactive-value="0"
- ></el-switch>
- <template v-if="state.login.model.captcha == 'geetest'">
- <div class="sa-title--line sa-m-b-30">极验验证码</div>
- <el-form-item label="极验Captchald" prop="captcha_geetest.captcha_id">
- placeholder="请输入Captchald"
- v-model="state.login.model.captcha_geetest.captcha_id"
- <el-form-item label="极验PrivateKey" prop="captcha_geetest.private_key">
- placeholder="请输入PrivateKey"
- v-model="state.login.model.captcha_geetest.private_key"
- <template v-if="configType == 'admin.config.basic.user'">
- :model="state.user.model"
- :rules="state.user.rules"
- ref="userRef"
- <el-form-item label="默认头像" prop="avatar">
- <sa-uploader v-model="state.user.model.avatar" fileType="image"></sa-uploader>
- <el-form-item label="默认昵称" prop="nickname">
- placeholder="请输入默认昵称"
- v-model="state.user.model.nickname"
- <template v-if="configType == 'admin.config.basic.mail'">
- :model="state.mail.model"
- :rules="state.mail.rules"
- ref="mailRef"
- <el-form-item label="邮件服务器" prop="smtp_host">
- placeholder="请输入邮件服务器"
- v-model="state.mail.model.smtp_host"
- <el-form-item label="邮件服务器端口" prop="smtp_port">
- placeholder="请输入邮件服务器端口"
- v-model="state.mail.model.smtp_port"
- <el-form-item label="邮件服务用户名" prop="smtp_user">
- placeholder="请输入邮件服务用户名"
- v-model="state.mail.model.smtp_user"
- <el-form-item label="邮件服务密码" prop="smtp_pass">
- placeholder="请输入邮件服务密码"
- v-model="state.mail.model.smtp_pass"
- <el-form-item label="发件人邮箱" prop="from">
- <el-input placeholder="请输入发件人邮箱" v-model="state.mail.model.from"></el-input>
- <el-form-item label="发件人姓名" prop="from_name">
- placeholder="请输入发件人姓名"
- v-model="state.mail.model.from_name"
- <el-form-item label="邮件服务验证方式">
- <el-select
- v-model="state.mail.model.verify_type"
- placeholder="请选择邮件服务验证方式"
- <el-option
- v-for="item in mailTemplate"
- :key="item.value"
- :label="item.name"
- :value="item.value"
- ></el-option>
- </el-select>
- <el-footer class="sa-footer--submit">
- <el-button @click="getData">重置</el-button>
- <el-button type="primary" @click="onUpdate">保存</el-button>
- </el-footer>
- name: 'admin.config.basic',
- import { reactive, ref, onMounted, computed, getCurrentInstance } from 'vue';
- import { checkAuth } from '@/sheep/directives/auth';
- const { proxy } = getCurrentInstance();
- const configList = computed(() => {
- const config = [
- { label: '站点配置', api: 'admin.config.basic.site' },
- { label: '登录配置', api: 'admin.config.basic.login' },
- { label: '用户配置', api: 'admin.config.basic.user' },
- { label: '邮件配置', api: 'admin.config.basic.mail' },
- ];
- return config.filter((c) => {
- if (checkAuth(c.api)) return c;
- const configType = ref(configList.value.length > 0 ? configList.value[0].api : '');
- const mailTemplate = [
- { name: '无', value: '' },
- { name: 'SSL', value: 'ssl' },
- { name: 'TLS', value: 'tls' },
- // 表单数据
- const state = reactive({
- site: {
- model: {},
- // rules: {
- // logo: [{ required: true, message: "请选择系统Logo", trigger: "blur" }],
- // name: [{ required: true, message: "请输入系统名称", trigger: "blur" }],
- // version: [{ required: true, message: "请输入版本号", trigger: "blur" }],
- // copyright: [
- // { required: true, message: "请输入版权信息", trigger: "blur" },
- // ],
- // copytime: [
- // { required: true, message: "请输入版权时间", trigger: "blur" },
- // cdnurl: [
- // { required: true, message: "请输入全站Cdn地址", trigger: "blur" },
- // beian: [{ required: true, message: "请输入备案号", trigger: "blur" }],
- // beian_url: [
- // { required: true, message: "请输入工信部网址", trigger: "blur" },
- // },
- login: {
- rules: {
- bg: [{ required: true, message: '请选择登录背景', trigger: 'blur' }],
- captcha_geetest: {
- captcha_id: [{ required: true, message: '请输入极验CaptchaId', trigger: 'blur' }],
- private_key: [{ required: true, message: '请输入极验PrivateKey', trigger: 'blur' }],
- user: {
- avatar: [{ required: true, message: '请选择默认头像', trigger: 'blur' }],
- nickname: [{ required: true, message: '请输入默认昵称', trigger: 'blur' }],
- mail: {
- smtp_host: [{ required: true, message: '请输入邮件服务器', trigger: 'blur' }],
- smtp_port: [{ required: true, message: '请输入邮件服务器端口', trigger: 'blur' }],
- smtp_user: [{ required: true, message: '请输入邮件服务用户名', trigger: 'blur' }],
- smtp_pass: [{ required: true, message: '请输入邮件服务密码', trigger: 'blur' }],
- from: [{ required: true, message: '请输入发件人邮箱', trigger: 'blur' }],
- from_name: [{ required: true, message: '请输入发件人姓名', trigger: 'blur' }],
- // 获取配置信息
- async function getData() {
- let type = configType.value.split('.').pop();
- const { data } = await admin.config.basic[type]();
- state[type].model = data;
- // 提交保存
- function onUpdate() {
- const form = proxy.$refs[type + 'Ref'];
- form.validate((valid) => {
- valid && admin.config.basic[type](state[type].model);
- if (configList.value.length > 0) {
@@ -1,459 +0,0 @@
- <el-tabs class="sa-tabs">
- <el-tab-pane label="短信配置"></el-tab-pane>
- :model="easysms.model"
- :rules="easysms.rules"
- ref="easysmsRef"
- <el-form-item label="默认发送网关" prop="gateways" required>
- <el-radio-group v-model="easysms.model.gateways">
- <el-radio label="aliyun">阿里云</el-radio>
- <el-radio label="qcloud">腾讯云</el-radio>
- <el-radio label="huawei">华为云</el-radio>
- <el-radio label="smsbao">短信宝</el-radio>
- <!-- 阿里云 -->
- <template v-if="easysms.model.gateways == 'aliyun'">
- <div class="sa-title--line sa-m-b-30">阿里云</div>
- <el-form-item label="AccessKeyId" prop="gateways_aliyun.access_key_id">
- placeholder="请输入AccessKeyId"
- v-model="easysms.model.gateways_aliyun.access_key_id"
- <el-form-item label="AccessKeySecret" prop="gateways_aliyun.access_key_secret">
- placeholder="请输入AccessKeySecret"
- v-model="easysms.model.gateways_aliyun.access_key_secret"
- <el-form-item label="短信签名" prop="gateways_aliyun.sign_name">
- placeholder="请输入短信签名"
- v-model="easysms.model.gateways_aliyun.sign_name"
- <!-- template 内容 -->
- <el-form-item label="短信模板">
- <div class="warning"> 请不要随意修改和删除下面的默认模板事件 </div>
- <div class="sa-template-wrap">
- <div class="title sa-flex">
- <div class="key">事件</div>
- <div class="value">模板ID</div>
- <div class="oper">操作</div>
- <sa-draggable
- v-model="easysms.model.gateways_aliyun.template"
- :animation="300"
- handle=".sortable-drag"
- item-key="element"
- <template #item="{ element, index }">
- <div class="item">
- <el-form-item
- class="key"
- :prop="'gateways_aliyun.template.' + index + '.event'"
- :rules="templateRules.event"
- <el-input placeholder="请输入键" v-model="element.event"></el-input>
- class="value"
- :prop="'gateways_aliyun.template.' + index + '.value'"
- :rules="templateRules.value"
- <el-input placeholder="请输入值" v-model="element.value"></el-input>
- <el-form-item class="oper">
- <el-button @click="onDeleteTemplate('aliyun', index)" type="danger" plain
- >删除</el-button
- <sa-svg class="sa-m-l-8 sortable-drag" name="sa-round"></sa-svg>
- </sa-draggable>
- <el-button
- @click="onAddTemplate('aliyun')"
- class="sa-m-l-16"
- type="primary"
- plain
- icon="Plus"
- >追加</el-button
- <!-- 腾讯云 -->
- <template v-if="easysms.model.gateways == 'qcloud'">
- <div class="sa-title--line sa-m-b-30">腾讯云</div>
- <!-- <el-form-item label="AppKey" prop="gateways_qcloud.app_key">
- placeholder="请输入AppKey"
- v-model="easysms.model.gateways_qcloud.app_key"
- <el-form-item label="SdkAppId" prop="gateways_qcloud.sdk_app_id">
- placeholder="请输入SdkAppId"
- v-model="easysms.model.gateways_qcloud.sdk_app_id"
- <el-form-item label="SecretId" prop="gateways_qcloud.secret_id">
- placeholder="请输入SecretId"
- v-model="easysms.model.gateways_qcloud.secret_id"
- <el-form-item label="SecretKey" prop="gateways_qcloud.secret_key">
- placeholder="请输入SecretKey"
- v-model="easysms.model.gateways_qcloud.secret_key"
- <el-form-item label="短信签名" prop="gateways_qcloud.sign_name">
- v-model="easysms.model.gateways_qcloud.sign_name"
- v-model="easysms.model.gateways_qcloud.template"
- :prop="'gateways_qcloud.template.' + index + '.event'"
- :prop="'gateways_qcloud.template.' + index + '.value'"
- <el-button @click="onDeleteTemplate('qcloud', index)" type="danger" plain
- @click="onAddTemplate('qcloud')"
- <!-- 华为云 -->
- <template v-if="easysms.model.gateways == 'huawei'">
- <div class="sa-title--line sa-m-b-30">华为云</div>
- <el-form-item label="Endpoint" prop="gateways_huawei.endpoint">
- placeholder="请输入Endpoint"
- v-model="easysms.model.gateways_huawei.endpoint"
- <el-form-item label="AppKey" prop="gateways_huawei.app_key">
- v-model="easysms.model.gateways_huawei.app_key"
- <el-form-item label="AppSecret" prop="gateways_huawei.app_secret">
- placeholder="请输入AppSecret"
- v-model="easysms.model.gateways_huawei.app_secret"
- <el-form-item label="短信签名" prop="gateways_huawei.sign_name">
- v-model="easysms.model.gateways_huawei.sign_name"
- v-model="easysms.model.gateways_huawei.template"
- :prop="'gateways_huawei.template.' + index + '.event'"
- :prop="'gateways_huawei.template.' + index + '.value'"
- <el-button @click="onDeleteTemplate('huawei', index)" type="danger" plain
- @click="onAddTemplate('huawei')"
- <!-- 短信宝 -->
- <template v-if="easysms.model.gateways == 'smsbao'">
- <div class="sa-title--line sa-m-b-30">短信宝</div>
- <el-form-item label="账号" prop="gateways_smsbao.user">
- placeholder="请输入账号"
- v-model="easysms.model.gateways_smsbao.user"
- <el-form-item label="密码" prop="gateways_smsbao.password">
- placeholder="请输入密码"
- v-model="easysms.model.gateways_smsbao.password"
- <el-form-item label="短信签名" prop="gateways_smsbao.sign_name">
- v-model="easysms.model.gateways_smsbao.sign_name"
- name: 'admin.config.easysms',
- import { onMounted, reactive, ref, unref } from 'vue';
- import SaDraggable from 'vuedraggable';
- const easysmsRef = ref();
- const easysms = reactive({
- gateways_aliyun: {
- access_key_id: [
- required: true,
- message: '请输入AccessKeyId',
- trigger: 'blur',
- access_key_secret: [
- message: '请输入AccessKeySecret',
- sign_name: [
- message: '请输入短信签名',
- gateways_qcloud: {
- app_key: [
- message: '请输入AppKey',
- sdk_app_id: [
- message: '请输入SdkAppId',
- gateways_huawei: {
- // 华为云
- app_secret: [
- message: '请输入AppSecret',
- endpoint: [
- message: '请输入Endpoint',
- gateways_smsbao: {
- user: [
- message: '请输入账号',
- password: [
- message: '请输入密码',
- const { data } = await admin.config.easysms();
- easysms.model = data;
- // 更新
- unref(easysmsRef).validate(async (valid) => {
- valid && admin.config.easysms(easysms.model);
- const templateRules = {
- event: [
- message: '请输入键',
- value: [
- message: '请输入值',
- function onAddTemplate(filed) {
- easysms.model['gateways_' + filed]['template'].push({
- event: '',
- function onDeleteTemplate(filed, index) {
- easysms.model['gateways_' + filed]['template'].splice(index, 1);
- .warning {
- margin-bottom: 8px;
- line-height: 32px;
- font-size: 14px;
@@ -1,301 +0,0 @@
- <el-form :model="form.model" :rules="form.rules" ref="formRef" label-width="100px">
- label="模板类型"
- v-if="
- props.modal.params.platform == 'WechatOfficialAccount' ||
- props.modal.params.platform == 'WechatMiniProgram'
- "
- <el-radio-group v-model="form.model.type">
- <el-radio label="default">默认配置</el-radio>
- <el-radio label="custom">自定义配置</el-radio>
- <div
- (form.model.type == 'default' &&
- props.modal.params.platform == 'WechatOfficialAccount') ||
- (form.model.type == 'default' && props.modal.params.platform == 'WechatMiniProgram')
- <el-form-item label="通知标题">
- v-model="form.model.name"
- placeholder="注册完成给上级发送"
- disabled
- <el-form-item label="模板编号" v-if="props.modal.params.platform != 'WechatMiniProgram'">
- <el-input v-model="form.model.wechat.temp_no" placeholder="" disabled></el-input>
- <el-form-item label="模板Id">
- <el-input v-model="form.model.content.template_id" disabled>
- <template v-if="checkAuth('admin.notification.config.gettemplateid')" #append>
- <span
- class="cursor-pointer"
- @click="getTemplateId('0')"
- v-if="!form.model.content.template_id"
- >立即获取</span
- <el-popover
- v-model:visible="refundPopover"
- :width="250"
- trigger="click"
- v-if="form.model.content.template_id"
- <div class="sa-flex sa-m-b-16">
- <el-icon class="question sa-m-r-8">
- <QuestionFilled />
- </el-icon>
- <div>确定要重新获取模板Id吗?</div>
- <el-checkbox
- v-model="isDelete"
- label="删除旧模板"
- class="sa-m-r-20"
- true-label="1"
- false-label="0"
- />
- <el-button size="small" @click="refundPopover = false">取消</el-button>
- <el-button size="small" type="primary" @click="getTemplateId(isDelete)"
- >确定</el-button
- <template #reference>
- <span>重新获取</span>
- </el-popover>
- <el-form-item label="模板" class="field">
- <div v-for="i in form.model.wechat.fields" :key="i" class="sa-flex">
- <div v-if="i.name">{{ i.name }}:</div>
- <div>{{ `{{${i.template_field}.DATA` }}}}</div>
- <div v-if="form.model.type == 'custom' || props.modal.params.platform == 'Sms'">
- <el-form-item label="模板消息Id">
- v-model="form.model.content.template_id"
- placeholder="请输入模版消息Id"
- v-for="(item, index) in form.model.content.fields"
- :key="item"
- class="sa-flex sa-m-b-24"
- <div class="field-name sa-flex sa-row-right sa-p-r-12">
- <span v-if="item.field">{{ item.name }}</span>
- v-else
- v-model="item.name"
- placeholder="请输入名称"
- class="sa-w-100"
- v-model="item.template_field"
- class="sa-w-168 sa-m-r-24"
- placeholder="请输入模版名称"
- <el-input v-model="item.value" class="sa-w-168" placeholder="请输入默认值"></el-input>
- v-if="!item.field"
- class="field-del sa-m-l-12 sa-flex cursor-pointer"
- @click.stop="fieldDel(index)"
- 删除
- <el-form-item>
- <el-button class="sa-m-l-12" icon="Plus" @click="addContent">添加</el-button>
- <div v-if="props.modal.params.platform == 'Email'">
- <div v-for="item in fieldList.data.fields" :key="item" class="sa-flex sa-m-b-24">
- v-model="item.field"
- <div class="warning-title"> 请按照如下格式在文档中插入要显示的字段 p:{字段名} </div>
- <sa-editor v-model:content="form.model.content"></sa-editor>
- <el-button v-auth="'admin.notification.config.edit'" type="primary" @click="confirm"
- >更新</el-button
- import SaEditor from '@/sheep/components/sa-editor/sa-editor.vue';
- const emit = defineEmits(['modalCallBack']);
- const props = defineProps({
- modal: {
- type: Object,
- const value1 = ref(false);
- let formRef = ref(null);
- const form = reactive({
- model: {
- type: 'custom',
- name: '',
- wechat: {},
- content: '',
- rules: {},
- const fieldList = reactive({
- data: {},
- const refundPopover = ref(false);
- const isDelete = ref('1');
- //删除
- function fieldDel(index) {
- form.model.content.fields.splice(index, 1);
- function addContent() {
- form.model.content.fields.push({
- template_field: '',
- //获取模板id
- async function getTemplateId(e) {
- refundPopover.value = false;
- const { data } = await admin.notification.getTemplateId(
- props.modal.params.event,
- props.modal.params.platform,
- { is_delete: e, template_id: e == 1 ? form.model.content.template_id : '' },
- form.model.content.template_id = data;
- // 获取详情
- async function getDetail() {
- const { data } = await admin.notification.config(
- form.model = data;
- if (props.modal.params.platform == 'Email') {
- fieldList.data = data.content;
- form.model.content = data.content_text;
- } else {
- form.model.content.fields.forEach((e) => {
- if (!e.value) {
- e['value'] = '';
- if (!e.template_field) {
- e['template_field'] = '';
- // 表单关闭时提交
- function confirm() {
- // 表单验证
- unref(formRef).validate(async (valid) => {
- if (!valid) return;
- delete form.model.content_text;
- let submitForm = cloneDeep(form.model);
- const { error } = await admin.notification.config(
- submitForm,
- if (error == 0) {
- emit('modalCallBack', { event: 'confirm' });
- getDetail();
- .header {
- width: 100%;
- height: 40px;
- color: #434343;
- padding-left: 16px;
- background: var(--sa-background-hex-hover);
- margin: 0 0 16px 0;
- font-weight: 500;
- .icon-warning {
- width: 14px;
- height: 14px;
- display: flex;
- margin-left: 8px;
- .openswitch {
- .switch-width {
- width: 40px;
- .warning-text {
- line-height: 16px;
- color: var(--sa-font);
- .add-richtext {
- .question {
- color: #faad14;
- font-size: 16px;
- .field-name {
- width: 100px;
- height: 32px;
- .field-del {
- color: #ff4d4f;
- .warning-title {
- color: rgb(255, 89, 89);
- margin-bottom: 10px;
- :deep() {
- .field {
- .el-form-item__content {
- display: block;
- border: 1px solid #d9d9d9;
- max-width: 360px;
- border-radius: 4px;
- padding: 6px 0 2px 12px;
- color: var(--sa-place);
@@ -1,327 +0,0 @@
- <el-container class="notificationConfig-page panel-block">
- <el-tabs class="sa-tabs" v-model="configType" @tab-change="getData()">
- <el-tab-pane label="通知用户" name="user"></el-tab-pane>
- <el-tab-pane label="通知管理员" name="admin"></el-tab-pane>
- <div class="label sa-flex">消息配置</div>
- <el-alert class="sa-alert">
- <template #title>
- <div class="sa-m-b-6">
- 1、消息通知仅用于向用户发送重要的服务通知,只能用于符合其要求的服务场景如:拼团成功通知、商品发货通知等。不支持广告等营销类消息以及其它所有可能对用户造成骚扰的消息;
- 2、小程序和公众号需要选择 生活服务/百货/超市/便利店 类目;
- <div class="sa-m-b-6">3、公众号和小程序必须选择上面对应的类目才可以自动获取模板。</div>
- <div>4、目前公众号类目模板库有待完善,部分模板无法自动获取,请自行在公众号后台->模板消息->挑选模板,并在商城后台->消息通知->对应消息的自定义配置处配置</div>
- </el-alert>
- <el-main class="sa-p-0 sa-p-t-20">
- <div class="sa-table-wrap" v-loading="loading">
- <template v-if="table.data.length > 0">
- <el-table height="100%" class="sa-table" :data="table.data" row-key="id" stripe>
- <el-table-column label="消息类别" min-width="160" align="center">
- {{ scope.row.name || '-' }}
- <el-table-column min-width="344">
- <template #header>
- <div class="sa-flex sa-row-center">
- <img
- src="/static/images/platform/wechat/officialAccount.png"
- class="header-icon sa-m-r-8"
- <div>公众号模版通知</div>
- <div v-if="scope.row.channels">
- v-auth="'admin.notification.config.detail'"
- color="#07C160"
- class="edit-btn"
- :disabled="!scope.row.channels.includes('WechatOfficialAccount')"
- @click="editRow(scope.row.event, 'WechatOfficialAccount')"
- >编辑配置</el-button
- <div class="sa-flex" v-if="scope.row.configs">
- <div class="official-time sa-flex sa-row-center sa-m-r-12">
- 已发送{{ scope.row.configs.WechatOfficialAccount?.send_num || 0 }}次
- <el-button v-auth="'admin.notification.config.setstatus'" link>
- v-if="scope.row.channels.includes('WechatOfficialAccount')"
- v-model="scope.row.configs.WechatOfficialAccount.status"
- active-value="enable"
- inactive-value="disabled"
- @change="setStatus($event, scope.row.event, 'WechatOfficialAccount')"
- <el-switch v-else model-value="false" disabled />
- class="sa-m-l-8"
- :class="
- scope.row.configs.WechatOfficialAccount?.status == 'enable' &&
- 'open-switch'
- >{{
- scope.row.configs.WechatOfficialAccount?.status == 'enable'
- ? '开启'
- : '关闭'
- }}
- </el-button>
- src="/static/images/platform/wechat/miniProgram.png"
- <div>微信小程序通知</div>
- color="#6F74E9"
- :disabled="!scope.row.channels.includes('WechatMiniProgram')"
- @click="editRow(scope.row.event, 'WechatMiniProgram')"
- <div class="official-time mini-time sa-flex sa-row-center sa-m-r-12">
- 已发送{{ scope.row.configs.WechatMiniProgram?.send_num || 0 }}次
- v-if="scope.row.channels.includes('WechatMiniProgram')"
- v-model="scope.row.configs.WechatMiniProgram.status"
- @change="setStatus($event, scope.row.event, 'WechatMiniProgram')"
- scope.row.configs.WechatMiniProgram?.status == 'enable' && 'open-switch'
- scope.row.configs.WechatMiniProgram?.status == 'enable' ? '开启' : '关闭'
- <img src="/static/images/notification/sms.png" class="header-icon sa-m-r-8" />
- <div>短信通知</div>
- color="#806AF6"
- :disabled="!scope.row.channels.includes('Sms')"
- @click="editRow(scope.row.event, 'Sms')"
- <div class="official-time sms-time sa-flex sa-row-center sa-m-r-12">
- 已发送{{ scope.row.configs.Sms?.send_num || 0 }}次
- v-if="scope.row.channels.includes('Sms')"
- v-model="scope.row.configs.Sms.status"
- @change="setStatus($event, scope.row.event, 'Sms')"
- :class="scope.row.configs.Sms?.status == 'enable' && 'open-switch'"
- >{{ scope.row.configs.Sms?.status == 'enable' ? '开启' : '关闭' }}</span
- <img src="/static/images/notification/email.png" class="header-icon sa-m-r-8" />
- <div>邮件通知</div>
- color="#409EFF"
- :disabled="!scope.row.channels.includes('Email')"
- @click="editRow(scope.row.event, 'Email')"
- <div class="official-time email-time sa-flex sa-row-center sa-m-r-12">
- 已发送{{ scope.row.configs.Email?.send_num || 0 }}次
- v-if="scope.row.channels.includes('Email')"
- v-model="scope.row.configs.Email.status"
- @change="setStatus($event, scope.row.event, 'Email')"
- :class="scope.row.configs.Email?.status == 'enable' && 'open-switch'"
- >{{ scope.row.configs.Email?.status == 'enable' ? '开启' : '关闭' }}</span
- <template v-if="table.data.length == 0 && !loading">
- <sa-empty></sa-empty>
- import { onMounted, reactive, ref, computed } from 'vue';
- import { useModal } from '@/sheep/hooks';
- import ConfigEdit from './edit.vue';
- const configType = ref('user');
- selected: [],
- let receiver_type = configType.value;
- const { error, data } = await admin.notification.config(receiver_type, null);
- error === 0 && (table.data = data);
- // 状态
- async function setStatus(val, e, type) {
- await admin.notification.setStatus(e, type, { status: val });
- function editRow(e, channel) {
- ConfigEdit,
- title: '编辑配置',
- type: 'edit',
- event: e,
- platform: channel,
- .notificationConfig-page {
- .el-main {
- background: var(--sa-background-assist);
- .sa-table-wrap {
- height: 100%;
- .header-icon {
- width: 16px;
- height: 16px;
- .edit-btn {
- border-radius: 4px 0 0 4px;
- color: var(--sa-background-assist);
- .official-time {
- width: 140px;
- background: rgba(7, 193, 96, 0.1);
- border-radius: 0 4px 4px 0;
- color: #07c160;
- .mini-time {
- background: rgba(111, 116, 233, 0.1);
- color: #6f74e9;
- .sms-time {
- background: rgba(128, 106, 246, 0.1);
- color: #806af6;
- .email-time {
- background: rgba(64, 158, 255, 0.1);
- color: #409eff;
- .open-switch {
- .el-alert__content {
- padding: 0;
- // .el-alert--info.is-light {
- // background: var(--sa-background-hex-hover);
- // }
@@ -1,314 +0,0 @@
- <el-tab-pane label="附件配置"></el-tab-pane>
- :model="filesystem.model"
- :rules="filesystem.rules"
- ref="filesystemRef"
- <el-form-item label="存储引擎" prop="driver" required>
- <el-radio-group v-model="filesystem.model.driver">
- <el-radio label="public">本地储存</el-radio>
- <el-radio label="aliyun">阿里云存储</el-radio>
- <el-radio label="qcloud">腾讯云存储</el-radio>
- <el-radio label="qiniu">七牛云储存</el-radio>
- <!-- 默认 -->
- <el-form-item label="允许上传类型" prop="extensions">
- placeholder="请输入允许上传类型(空格符为逗号)"
- v-model="filesystem.model.extensions"
- <el-form-item label="文件上传限制" prop="filesize">
- <el-input placeholder="请输入文件上传限制" v-model="filesystem.model.filesize">
- <template #append>M</template>
- <el-form-item label="图片上传限制" prop="imagesize">
- <el-input placeholder="请输入图片上传限制" v-model="filesystem.model.imagesize">
- <!-- 本地存储 -->
- <template v-if="filesystem.model.driver == 'public'">
- <div class="sa-title--line sa-m-b-30">本地存储</div>
- <el-form-item label="访问域名">
- placeholder="请输入访问域名 默认使用当前域名"
- v-model="filesystem.model.disks.public.url"
- <template v-if="filesystem.model.driver == 'aliyun'">
- <div class="sa-title--line sa-m-b-30">阿里云存储</div>
- <el-form-item label="访问域名" prop="disks.aliyun.url">
- placeholder="请输入访问域名"
- v-model="filesystem.model.disks.aliyun.url"
- <el-form-item label="AccessId" prop="disks.aliyun.access_id">
- placeholder="请输入AccessId"
- v-model="filesystem.model.disks.aliyun.access_id"
- <el-form-item label="AccessSecret" prop="disks.aliyun.access_secret">
- placeholder="请输入AccessSecret"
- v-model="filesystem.model.disks.aliyun.access_secret"
- <el-form-item label="Bucket" prop="disks.aliyun.bucket">
- placeholder="请输入Bucket"
- v-model="filesystem.model.disks.aliyun.bucket"
- <el-form-item label="Endpoint" prop="disks.aliyun.endpoint">
- v-model="filesystem.model.disks.aliyun.endpoint"
- <div v-if="filesystem.model.driver == 'qcloud'">
- <div class="sa-title--line sa-m-b-30">腾讯云存储</div>
- <el-form-item label="访问域名" prop="disks.qcloud.url">
- v-model="filesystem.model.disks.qcloud.url"
- <el-form-item label="所属区域" prop="disks.qcloud.region">
- placeholder="请输入所属区域"
- v-model="filesystem.model.disks.qcloud.region"
- <el-form-item label="AppId" prop="disks.qcloud.app_id">
- placeholder="请输入AppId"
- v-model="filesystem.model.disks.qcloud.app_id"
- <el-form-item label="SecretId" prop="disks.qcloud.secret_id">
- v-model="filesystem.model.disks.qcloud.secret_id"
- <el-form-item label="SecretKey" prop="disks.qcloud.secret_key">
- v-model="filesystem.model.disks.qcloud.secret_key"
- <el-form-item label="Bucket" prop="disks.qcloud.bucket">
- v-model="filesystem.model.disks.qcloud.bucket"
- <!-- 七牛云 -->
- <div v-if="filesystem.model.driver == 'qiniu'">
- <div class="sa-title--line sa-m-b-30">七牛云存储</div>
- <el-form-item label="访问域名" prop="disks.qiniu.url">
- v-model="filesystem.model.disks.qiniu.url"
- <el-form-item label="AccessKey" prop="disks.qiniu.access_key">
- placeholder="请输入AccessKey"
- v-model="filesystem.model.disks.qiniu.access_key"
- <el-form-item label="SecretKey" prop="disks.qiniu.secret_key">
- v-model="filesystem.model.disks.qiniu.secret_key"
- <el-form-item label="Bucket" prop="disks.qiniu.bucket">
- v-model="filesystem.model.disks.qiniu.bucket"
- name: 'file.admin.config',
- import { reactive, ref, onMounted, unref } from 'vue';
- import file from '@/app/file/api';
- // 附件数据
- const filesystemRef = ref();
- const filesystem = reactive({
- extensions: [{ required: true, message: '请输入允许上传类型', trigger: 'blur' }],
- filesize: [{ required: true, message: '请输入文件上传限制', trigger: 'blur' }],
- imagesize: [{ required: true, message: '请输入图片上传限制', trigger: 'blur' }],
- disks: {
- aliyun: {
- access_id: [
- message: '请输入AccessId',
- access_secret: [
- message: '请输入AccessSecret',
- bucket: [
- message: '请输入Bucket',
- url: [
- message: '请输入访问域名',
- qcloud: {
- app_id: [
- message: '请输入AppId',
- secret_bucket: [
- region: [
- message: '请输入所属区域',
- secret_id: [
- message: '请输入SecretId',
- secret_key: [
- message: '请输入SecretKey',
- qiniu: {
- access_key: [
- message: '请输入AccessKey',
- const { data } = await file.config.filesystem();
- filesystem.model = data;
- unref(filesystemRef).validate((valid) => {
- valid && file.config.filesystem(filesystem.model);
@@ -1,13 +0,0 @@
- <sa-filemanager />
- name: 'file.admin',
- import SaFilemanager from '@/sheep/components/sa-file/sa-filemanager.vue';
@@ -1,55 +0,0 @@
-import { request } from '@/sheep/request';
-import { CRUD } from '@/sheep/request/crud';
-import { isEmpty } from 'lodash';
-export default {
- // 文件管理
- ...CRUD('file/admin/file'),
- upload: (params, data) =>
- request({
- url: 'file/admin/file/upload',
- method: 'POST',
- headers: {
- 'Content-Type': 'multipart/form-data',
- timeout: 80000,
- params,
- data,
- }),
- uploadCert: (data) =>
- url: 'file/admin/file/uploadCert',
- rename: (id, data) =>
- url: `file/admin/file/rename/${id}`,
- move: (id, data) =>
- url: `file/admin/file/move/${id}`,
- group: {
- ...CRUD('file/admin/group'),
- config: {
- // 附件配置
- filesystem: (data) =>
- url: 'file/admin/config/filesystem',
- method: isEmpty(data) ? 'GET' : 'PUT',
- options: {
- showSuccessMessage: !isEmpty(data),
-};
@@ -1,35 +0,0 @@
-import Layout from '@/sheep/layouts/index.vue';
- path: '/file',
- name: 'file',
- component: Layout,
- children: [
- path: 'admin',
- redirect: '/file/admin/index',
- meta: {
- title: '文件管理',
- path: 'index',
- name: 'file.admin.index',
- component: () => import('@/app/file/admin/index.vue'),
- path: 'config',
- component: () => import('@/app/file/admin/config.vue'),
- title: '附件配置',
@@ -1,30 +0,0 @@
- check: () =>
- url: '/admin/install/check',
- init: () =>
- url: '/admin/install',
- method: 'GET',
- install: (data) =>
- showSuccessMessage: true,
- timeout: 60000,
- checkLicenseKey: (params) =>
- url: '/admin/install/checkLicenseKey',
@@ -1,835 +0,0 @@
- <el-container v-loading="state.loading">
- <el-header class="header-box">
- <div class="install-header sa-flex sa-col-center sa-row-between">
- <div class="">
- <img class="install-header-logo" src="/static/images/install/logo.png" />
- <div class="install-header-list sa-flex sa-row-center">
- <div class="item-title" :class="{ 'activite-title': state.currentStep === 'protocol' }">
- 用户协议
- <div class="item-line"></div>
- <div class="item-title" :class="{ 'activite-title': state.currentStep === 'checkEnv' }">
- 检测环境
- <div class="item-title" :class="{ 'activite-title': state.currentStep === 'license' }">
- 授权码
- <div class="item-title" :class="{ 'activite-title': state.currentStep === 'setting' }">
- 配置
- <div class="item-title" :class="{ 'activite-title': state.currentStep === 'install' }">
- 安装
- <div class="install-header-link sa-flex sa-row-right">
- <button class="sa-reset-button official-btn" @click="onOfficialSite">官网</button>
- <div class="main-box">
- <el-row>
- <el-col :md="3" :lg="5"></el-col>
- <el-col v-if="!state.installed" :md="18" :lg="14">
- <!-- 阅读协议 -->
- <div class="main-content sa-p-24" v-if="state.currentStep === 'protocol'">
- <div class="sa-flex sa-m-b-38">
- <div class="main-line sa-m-r-10"></div>
- <div class="main-content-headline">用户协议</div>
- <div class="richtext-title">
- 作者:SheepJS团队<br />
- 更新时间:2022-10-12<br /><br />
- 感谢您选择SheepJS - Shopro商城(以下简称Shopro,Shopro基于 PHP +
- MySQL的技术开发,全部源码开放。
- 为了使你正确并合法的使用本软件,请你在使用前务必阅读清楚下面的协议条款:<br /><br />
- 一、本授权协议适用且仅适用于Shopro商城(Shopro商城是由SheepJS团队开发并支持的一套完整B2C商城系统.
- 以下简称Shopro)任何版本,SheepJS官方对本授权协议的最终解释权。<br /><br />
- 二、协议许可的权利<br />
- 您可以在支付商业域名授权完成并完全遵守本最终用户授权协议的基础上,将本软件应用于商业用途,而不必支付其它软件版权授权费用。
- 您可以在协议规定的约束和限制范围内修改Shopro源代码或界面风格以适应您的网站要求。
- 您拥有使用本软件构建的网站全部内容所有权,并独立承担与这些内容的相关法律义务。
- 获得商业授权之后,您可以将本软件应用于商业用途,同时依据所购买的授权类型中确定的技术支持内容,自购买时刻起,在技术支持期限内拥有通过指定的方式获得指定范围内的技术支持服务。商业授权用户享有反映和提出意见的权力,相关意见将被作为首要考虑,但没有一定被采纳的承诺或保证。<br /><br />
- 三、协议规定的约束和限制<br />
- 未获商业授权之前,不得将本软件用于商业用途(包括但不限于企业网站、经营性网站、以营利为目的或实现盈利的网站)。
- 未经官方许可,不得对本软件或与之关联的商业授权进行出租、出售、抵押或发放子许可证。
- 未经官方许可,禁止在Shopro的整体或任何部分基础上以发展任何派生版本、修改版本或第三方版本用于重新分发。
- 如果您未能遵守本协议的条款,您的授权将被终止,所被许可的权利将被收回,并承担相应法律责任。<br /><br />
- 四、有限担保和免责声明<br />
- 本软件及所附带的文件是作为不提供任何明确的或隐含的赔偿或担保的形式提供的。
- 用户出于自愿而使用本软件,您必须了解使用本软件的风险,在尚未购买产品技术服务之前,我们不承诺对免费用户提供任何形式的技术支持、使用担保,也不承担任何因使用本软件而产生问题的相关责任。
- 电子文本形式的授权协议如同双方书面签署的协议一样,具有完全的和等同的法律效力。您一旦开始确认本协议并安装
- Shopro,即被视为完全理解并接受本协议的各项条款,在享有上述条款授予的权力的同时,受到相关的约束和限制。协议许可范围以外的行为,将直接违反本授权协议并构成侵权,我们有权随时终止授权,责令停止损害,并保留追究相关责任的权力。
- 如果本软件带有其它软件的整合API示范例子包,这些文件版权不属于本软件官方,并且这些文件是没经过授权发布的,请参考相关软件的使用许可合法的使用。<br /><br />
- 2021~2022 SheepJS™团队版权所有并保留所有权利。
- <!-- 检查环境 -->
- <div class="main-content sa-p-24" v-if="state.currentStep === 'checkEnv'">
- <div class="sa-flex sa-m-b-28">
- <div class="main-content-headline">系统环境</div>
- <div class="sa-flex-col sa-col-center">
- v-for="i in state.checkEnvResult.output"
- :key="i"
- class="sa-flex sa-col-center sa-m-b-28"
- <div class="output-title">
- {{ i.item }}
- <div class="icon-box">
- <el-icon class="success-icon" v-if="i.error === 0"
- ><CircleCheckFilled
- /></el-icon>
- <el-icon class="filled-icon" v-if="i.error === 1"
- ><CircleCloseFilled
- <el-icon class="warning-icon" v-if="i.error === 2"><WarningFilled /></el-icon>
- <div class="output-msg">
- {{ i.msg }}
- <!-- 授权码 -->
- <div v-if="state.currentStep === 'license'">
- <div class="main-content sa-p-24">
- <div class="main-content-headline">授权信息</div>
- :model="form.model"
- :rules="form.rules"
- ref="formRef"
- label-width="120px"
- label-position="right"
- <el-form-item label="授权码" prop="license_key">
- class="sa-w-240"
- :rows="4"
- type="textarea"
- placeholder="请填写或粘贴您的授权码"
- v-model="form.model.license_key"
- <div class="license-text sa-m-t-40">
- 1.请填写您已备案的授权码,该授权码需与当前站点的线上域名保持一致
- 2.如遗失或暂无授权码,请前往
- <button class="sa-reset-button buy-link" @click="onBuy">查询或购买</button>
- 3.请勿分享或使用其他任何形式获取的授权码,以此带来的纠纷或法律问题将由您或贵司自行承担<br />
- 4.尊重原创,支持正版,我们将保持持续的更新与服务
- <!-- 配置 -->
- <div v-if="state.currentStep === 'setting'">
- <el-collapse v-model="state.showMysql">
- <el-collapse-item name="showMysql">
- <div class="main-content-headline">MySQL</div>
- <div class="main-content-headline"></div>
- <el-form-item label="数据库地址" prop="mysql_hostname">
- <el-input class="sa-w-240" v-model="form.model.mysql_hostname"></el-input>
- <el-form-item label="端口" prop="mysql_hostport">
- <el-input class="sa-w-240" v-model="form.model.mysql_hostport"></el-input>
- <el-form-item label="数据库名" prop="mysql_database">
- <el-input class="sa-w-240" v-model="form.model.mysql_database"></el-input>
- <el-form-item label="登录账号" prop="mysql_username">
- <el-input class="sa-w-240" v-model="form.model.mysql_username"></el-input>
- <el-form-item label="登录密码">
- <el-input class="sa-w-240" v-model="form.model.mysql_password"></el-input>
- <el-form-item label="数据库表前缀">
- <el-input class="sa-w-240" v-model="form.model.mysql_prefix"></el-input>
- <el-form-item label="安装测试数据">
- v-model="form.model.is_test_data"
- style="--el-switch-on-color: #806af6; --el-switch-off-color: #bfbfbf"
- </el-collapse-item>
- </el-collapse>
- <div class="redis-box sa-m-t-24">
- <el-collapse v-model="state.showRedis" @change="onRedis">
- <el-collapse-item name="showRedis">
- <div class="redis-box-header sa-flex sa-row-between">
- <div class="redis-box-title">Redis</div>
- v-model="form.model.check_redis"
- <div></div>
- <el-form-item label="redis地址" prop="redis_host">
- <el-input class="sa-w-240" v-model="form.model.redis_host"></el-input>
- <el-form-item label="端口" prop="redis_port">
- <el-input class="sa-w-240" v-model="form.model.redis_port"></el-input>
- <el-form-item label="连接密码">
- <el-input class="sa-w-240" v-model="form.model.redis_password"></el-input>
- <el-form-item label="db库1-15" prop="redis_select">
- <el-input class="sa-w-240" v-model="form.model.redis_select"></el-input>
- <div class="main-content sa-p-24 sa-m-t-24">
- <div class="main-content-headline">登录信息</div>
- <el-form-item label="登录账号" prop="admin_account">
- <el-input class="sa-w-240" v-model="form.model.admin_account"></el-input>
- <el-form-item label="登录密码" prop="admin_password">
- <el-input class="sa-w-240" v-model="form.model.admin_password"></el-input>
- <!-- 安装成功 -->
- class="install-box sa-flex-col sa-row-center sa-col-center"
- v-if="state.currentStep === 'install'"
- <div class="ss-m-b-60">
- <img class="install-img" src="/static/images/install/img-01.png" />
- <div class="content sa-flex-col sa-row-center sa-col-center">
- <div class="content-title sa-m-t-32 sa-m-b-18">系统信息</div>
- <div class="content-msg sa-flex sa-row-between sa-m-b-40">
- <div class="content-msg-title">
- <div class="sa-m-b-12">后台访问地址:</div>
- <div>{{ pathname }}</div>
- <button class="sa-reset-button sa-m-l-10" @click="onCopy()">复制</button>
- <div class="sa-m-b-12">登录账号:</div>
- <div>{{ form.model.admin_account }}</div>
- <button class="sa-reason-button sa-m-b-24 goback-btn" @click="onHome">
- 进入后台
- </button>
- <div class="sa-m-t-24">
- <div class="sa-flex sa-row-center install-foot-title sa-m-b-12">
- <a class="link sa-m-r-60" href="https://www.sheepjs.com/" target="_blank"
- >官方网站</a
- <a class="link" href="https://doc.sheepjs.com/" target="_blank">开发文档</a>
- <div class="install-foot-title"> Copyright © 星品科技 All Rights Reserverd. </div>
- <el-col v-else :md="18" :lg="14">
- <!-- 已安装 -->
- <div class="install-msg">
- {{ state.installErrorMessage }}
- <el-footer>
- <div class="foot-box" v-if="state.currentStep != 'install'">
- <el-col :md="18" :lg="14">
- v-if="state.installed"
- class="foot-content sa-flex sa-row-right sa-col-center sa-p-r-20"
- <el-button type="primary" @click="onHome">返回首页</el-button>
- <div v-else class="foot-content sa-flex sa-row-right sa-col-center sa-p-r-20">
- <el-button v-if="showLastStep" type="default" plain @click="onLastStep"
- >上一步</el-button
- <el-button v-if="state.currentStep === 'protocol'" type="primary" @click="onAgree"
- >同意并继续</el-button
- v-if="state.currentStep === 'checkEnv' && !state.loading"
- type="default"
- @click="onCheckEnv"
- >重新检测</el-button
- v-if="state.currentStep === 'checkEnv' && state.loading"
- >检测中...</el-button
- state.currentStep === 'checkEnv' &&
- state.checkEnvResult.error === 0 &&
- !state.loading
- @click="onNextStep('license')"
- >下一步</el-button
- v-if="state.currentStep === 'license'"
- @click="onCheckLicense"
- >验证授权</el-button
- v-if="state.currentStep === 'setting'"
- @click="onSubmitInstall"
- >确认安装</el-button
- v-if="state.currentStep === 'setting' && state.loading"
- >安装中...</el-button
- name: 'install.admin',
- import { reactive, onMounted, unref, ref, computed } from 'vue';
- import installApi from '@/app/install/api';
- import handleClipboard from '@/sheep/utils/clipboard';
- // 步进映射
- const stepMap = ['protocol', 'checkEnv', 'license', 'setting', 'install'];
- loading: true, // 加载状态
- installed: true, // 应用是否安装
- installErrorMessage: '', // 安装错误信息
- currentStep: 'protocol', // 当前步骤
- checkEnvResult: {},
- showMysql: 'showMysql',
- showRedis: 'showRedis',
- // 是否显示上一步
- const showLastStep = computed(() => {
- const currentIndex = stepMap.findIndex((item) => state.currentStep === item);
- if (currentIndex > 0 && currentIndex < stepMap.length) {
- return true;
- return false;
- // 点击上一步
- function onLastStep() {
- state.currentStep = stepMap[currentIndex - 1];
- // 点击下一步
- function onNextStep(tabName) {
- state.currentStep = tabName;
- function onRedis(e) {
- if (e[0] === 'showRedis') {
- form.model.check_redis = 1;
- form.model.check_redis = 0;
- function onCopy() {
- handleClipboard(pathname);
- // 添加 编辑 form
- license_key: '',
- mysql_database: '',
- mysql_hostname: '',
- mysql_hostport: '',
- mysql_password: '',
- mysql_prefix: '',
- mysql_username: '',
- check_redis: 0,
- redis_host: '',
- redis_password: '',
- redis_port: '',
- redis_select: '',
- admin_account: '',
- admin_password: '',
- mysql_hostname: [{ required: true, message: '请输入数据库地址', trigger: 'blur' }],
- mysql_hostport: [{ required: true, message: '请输入端口', trigger: 'blur' }],
- mysql_database: [{ required: true, message: '请输入数据库名', trigger: 'blur' }],
- mysql_username: [{ required: true, message: '请输入登录账号', trigger: 'blur' }],
- redis_host: [{ required: true, message: '请输入redis地址', trigger: 'blur' }],
- redis_port: [{ required: true, message: '请输入端口', trigger: 'blur' }],
- redis_select: [{ required: true, message: '请输入登录账号', trigger: 'blur' }],
- admin_account: [{ required: true, message: '请输入登录账号', trigger: 'blur' }],
- admin_password: [{ required: true, message: '请输入登录密码', trigger: 'blur' }],
- license_key: [{ required: true, message: '请输入授权码', trigger: 'blur' }],
- const pathname = window.location.origin + window.location.pathname;
- // 去官网
- function onOfficialSite() {
- window.open('https://www.sheepjs.com', '_blank');
- // 去授权
- function onBuy() {
- window.open('https://www.sheepjs.com/buy', '_blank');
- // 同意并继续
- function onAgree() {
- state.currentStep = 'checkEnv';
- onCheckEnv();
- // 返回首页
- function onHome() {
- window.location.href = window.location.pathname;
- // 检测服务端环境
- async function onCheckEnv() {
- state.loading = true;
- const { error, data } = await installApi.check();
- setTimeout(function () {
- state.loading = false;
- }, 300);
- state.checkEnvResult = data;
- // 验证授权
- async function onCheckLicense() {
- let res = await installApi.checkLicenseKey({ license_key: form.model.license_key });
- if (res.error === 0) {
- state.currentStep = 'setting';
- async function getInit() {
- const res = await installApi.init();
- form.model = {
- ...form.model,
- ...res.data.mysql,
- ...res.data.redis,
- state.installed = false;
- state.installErrorMessage = res.msg || '网络出现问题';
- if (form.model.check_redis === 1) {
- state.showRedis = 'showRedis';
- function onSubmitInstall() {
- // state.loading = true;
- submitForm.is_install = 1;
- const { error } = await installApi.install(submitForm);
- state.currentStep = 'install';
- getInit();
- a:visited {
- color: #fff;
- .el-collapse {
- border: none;
- box-shadow: 0 4px 18px rgba(0, 0, 0, 0.08);
- border-radius: 8px;
- padding: 0 24px;
- --el-collapse-content-bg-color: #ffffff;
- --el-collapse-header-bg-color: #ffffff;
- .redis-box {
- :deep(.el-collapse-item__arrow) {
- display: none;
- .redis-box-header {
- .redis-box-title {
- color: #262626;
- font-weight: 600 !important;
- font-size: 18px;
- line-height: 20px;
- .el-form-item {
- justify-content: center;
- flex: none;
- .el-collapse-item__header {
- border-bottom: none;
- font-weight: 600;
- .el-collapse-item__wrap {
- .el-form-item__label {
- .el-textarea__inner {
- .el-form-item__error {
- .el-menu--horizontal > .el-menu-item.is-active {
- border-bottom: 3px solid #ffffff;
- .header-box {
- z-index: 2001;
- --el-header-padding: 0;
- .install-header {
- height: 60px;
- background: var(--el-color-primary);
- box-sizing: border-box;
- padding: 0 40px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.16);
- position: fixed;
- left: 0;
- top: 0;
- z-index: 10;
- .official-btn {
- color: #fdfdfd;
- font-size: 20px;
- line-height: 24px;
- .install-header-logo {
- width: 146px;
- .install-header-link {
- .install-header-list {
- .menu-box {
- border-bottom: 0;
- .item-title {
- margin-right: 40px;
- align-items: center;
- color: #d7d1fb;
- cursor: default;
- :last-child {
- margin-right: 0;
- .activite-title {
- position: relative;
- .item-line {
- position: absolute;
- left: 50%;
- transform: translateX(-50%);
- bottom: 0;
- height: 3px;
- background: #ffffff;
- .install-box {
- height: 628px;
- background: var(--el-color-primary) url('/static/images/install/bg.png') no-repeat top center /
- 100% auto;
- .install-img {
- width: 200px;
- height: 200px;
- .content {
- width: 72%;
- background: #8d79f7;
- border-radius: 12px;
- .content-title {
- font-size: 24px;
- .content-msg {
- width: 80%;
- .content-msg-title {
- line-height: 18px;
- .goback-btn {
- width: 104px;
- background: #fdfdfd;
- .install-foot-title {
- font-size: 12px;
- .link {
- .main-line {
- width: 4px;
- height: 30px;
- .main-content-headline {
- .license-text {
- color: #8c8c8c;
- font-weight: 400;
- .buy-link {
- .install-msg {
- height: calc(100vh - 160px);
- .main-content {
- .richtext-title {
- color: #595959;
- width: inherit;
- text-align: justify;
- // white-space: pre-line;
- overflow: hidden;
- word-wrap: break-word;
- .richtext-title-headline {
- text-align: center;
- .icon-box {
- margin: 0 16px 0 32px;
- .success-icon {
- color: #52c41a;
- .filled-icon {
- .warning-icon {
- .output-title {
- width: 220px;
- justify-content: flex-end;
- .output-msg {
- width: 300px;
- .foot-box {
- box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.08);
- .foot-content {
- @media only screen and (max-width: 768px) {
- padding: 0 30px;
- margin: 0 24px;
- margin: 0 12px 0 20px !important;
- font-size: 14px !important;
@@ -1,12 +0,0 @@
-import Layout from '@/sheep/layouts/content.vue';
- path: '/install',
- name: 'install',
- component: () => import('@/app/install/index.vue'),
- title: '安装',
- login: false,
- taskbar: false,
@@ -1,128 +0,0 @@
-import Content from '@/sheep/layouts/content.vue';
-import { CRUD, RECYCLE } from '@/sheep/request/crud';
-const route = {
- path: 'app',
- name: 'shop.admin.app',
- component: Content,
- title: '应用',
- path: 'scoreshop',
- name: 'shop.admin.app.scoreshop',
- component: () => import('./scoreShop/index.vue'),
- title: '积分商城',
- path: 'mplive',
- name: 'shop.admin.app.mplive',
- component: () => import('./mplive/index.vue'),
- title: '小程序直播',
-const api = {
- scoreShop: {
- ...CRUD('shop/admin/app/scoreShop', ['list', 'add', 'edit', 'delete']),
- ...RECYCLE('shop/admin/app/scoreShop'),
- getSkuPrices: (id) =>
- url: '/shop/admin/app/scoreShop/skuPrices/' + id,
- skus: (id) =>
- url: `/shop/admin/app/scoreShop/skus/${id}`,
- select: (params, type = 'page') =>
- url: `shop/admin/app/scoreShop/select?type=${type}`,
- mplive: {
- room: {
- ...CRUD('shop/admin/app/mplive/room', ['list', 'detail', 'delete']),
- add: (data) =>
- url: '/shop/admin/app/mplive/room',
- edit: (id, data) =>
- url: '/shop/admin/app/mplive/room/' + id,
- method: 'PUT',
- select: (params) =>
- url: `shop/admin/app/mplive/room/select`,
- sync: () =>
- url: '/shop/admin/app/mplive/room/sync',
- pushUrl: (id) =>
- url: '/shop/admin/app/mplive/room/pushUrl/' + id,
- qrcode: (id) =>
- url: '/shop/admin/app/mplive/room/qrcode/' + id,
- playback: (id) =>
- url: '/shop/admin/app/mplive/room/playback/' + id,
- goods: {
- ...CRUD('shop/admin/app/mplive/goods', ['list', 'detail', 'delete']),
- url: '/shop/admin/app/mplive/goods',
- url: '/shop/admin/app/mplive/goods/' + id,
- audit: (id, data) =>
- url: '/shop/admin/app/mplive/goods/audit/' + id,
- status: (id) =>
- url: '/shop/admin/app/mplive/goods/status/' + id,
-export { route, api };
@@ -1,267 +0,0 @@
- <el-form :model="form.model" :rules="form.rules" ref="formRef" label-width="120px">
- <div class="sa-color--warning sa-m-l-50 sa-m-b-18"
- >在小程序直播控制台添加的商品,不支持通过此系统操作。</div
- <el-form-item label="商品来源">
- <el-radio :label="0">我的小程序</el-radio>
- <el-radio :label="1">其他小程序</el-radio>
- <el-form-item label="商品选择" prop="goods_id" v-if="form.model.type === 0">
- <el-button type="primary" class="is-link" @click="selectGoods" v-if="!form.model.goods_id"
- >选择商品</el-button
- <div class="sa-template-content sa-template-goods" v-if="form.model.goods_id">
- <div class="header sa-flex">
- <div class="item">商品信息</div>
- <div class="list sa-flex">
- <div class="item sa-flex">
- <sa-image :url="goods.image" size="40"></sa-image>
- <div class="sa-m-l-8">
- <div class="title">{{ goods.title }}</div>
- <div class="oper">
- <el-button class="is-link" type="danger" @click="deleteItems()">移除</el-button>
- <el-form-item label="商品封面">
- <sa-uploader v-model="form.model.cover_img_url" fileType="image" isCropper></sa-uploader>
- <div class="desc sa-m-l-12"> 图片尺寸最大300像素*300像素 </div>
- <el-form-item label="商品名称" prop="name">
- :maxlength="14"
- :minlength="3"
- show-word-limit
- placeholder="请输入商品名称"
- <el-form-item label="价格形式" prop="price_type">
- <el-radio-group v-model="form.model.price_type">
- <el-radio :label="1">一口价</el-radio>
- <el-radio :label="2">价格区间</el-radio>
- <el-radio :label="3">显示折扣价</el-radio>
- <el-form-item label="价格">
- <div class="sa-m-r-8" v-if="form.model.price_type !== 1">{{
- form.model.price_type === 2 ? '最小价格' : '市场价'
- }}</div>
- v-model="form.model.price"
- placeholder="请输入价格"
- class="sa-w-160"
- type="number"
- :step="0.01"
- :min="0"
- :precision="2"
- <template #append>元</template>
- <div class="sa-m-l-16 sa-m-r-8" v-if="form.model.price_type !== 1">{{
- form.model.price_type === 2 ? '最大价格' : '现价'
- v-model="form.model.price2"
- v-if="form.model.price_type !== 1"
- <el-form-item label="APPID" prop="third_party_appid" v-if="form.model.type === 1">
- v-model="form.model.third_party_appid"
- placeholder="请输入该小程序APPID"
- <el-form-item label="商品路径" prop="url">
- <el-input v-model="form.model.url" placeholder="请输入商品路径"></el-input>
- <div class="desc">
- 请确保小程序页面路径可被访问。例如:pages/goods/index?query=value
- v-if="modal.params.type == 'add'"
- v-auth="'shop.admin.app.mplive.goods.add'"
- @click="confirm"
- >保存</el-button
- v-if="modal.params.type == 'edit'"
- v-auth="'shop.admin.app.mplive.goods.edit'"
- import { api } from '../../app.service';
- import GoodsSelect from '@/app/shop/admin/goods/goods/select.vue';
- import { api as goodsApi } from '@/app/shop/admin/goods/goods.service';
- type: 0,
- price_type: 1,
- third_party_appid: '',
- price: '',
- price2: '',
- cover_img_url: '',
- name: [{ validator: checkTitle, trigger: 'change' }],
- cover_img_url: [{ required: true, message: '请选择商品封面', trigger: 'change' }],
- const goods = reactive({
- id: '', // 商品id
- image: '', // 商品图片
- title: '', // 商品名称
- const loading = ref(false);
- function checkTitle(rule, value, callback) {
- if (!value) {
- return callback(new Error('请输入商品名称'));
- const length =
- value.match(/[^ -~]/g) == null ? value.length : value.length + value.match(/[^ -~]/g).length;
- if (length < 6 || length > 28) {
- callback(new Error('直播标题必须为3-14个字(一个字等于两个英文字符或特殊字符)'));
- callback();
- // 获取商品信息
- async function getGoodsList(id) {
- const { data } = await goodsApi.goods.select(
- search: JSON.stringify({ id }),
- 'select',
- goods.image = data[0].image;
- goods.title = data[0].title;
- async function getDetail(id) {
- const { error, data } = await api.mplive.goods.detail(id);
- getGoodsList(form.model.goods_id);
- async function confirm() {
- const { error } =
- props.modal.params.type == 'add'
- ? await api.mplive.goods.add(submitForm)
- : await api.mplive.goods.edit(props.modal.params.id, submitForm);
- //选择商品
- function selectGoods() {
- let id = '';
- GoodsSelect,
- title: '选择商品',
- id,
- confirm: (res) => {
- goods.image = res.data.image;
- goods.title = res.data.title;
- goods.id = res.data.id;
- form.model.cover_img_url = res.data.image;
- form.model.name = res.data.title;
- form.model.url = 'pages/goods/index?id=' + goods.id;
- if (res.data.price.length === 2) {
- form.model.price_type = 2;
- form.model.price = res.data.price[0];
- form.model.price2 = res.data.price[1];
- if (Number(res.data.original_price)) {
- form.model.price_type = 3;
- form.model.price = res.data.original_price;
- form.model.price2 = res.data.price[0];
- form.model.price_type = 1;
- form.model.goods_id = res.data.id;
- function deleteItems() {
- form.model.goods_id = '';
- async function init() {
- if (props.modal.params.id) {
- await getDetail(props.modal.params.id);
- init();
- .desc {
- color: #999999;
@@ -1,294 +0,0 @@
- <span class="left">商品库管理</span>
- class="sa-button-refresh"
- @click="addGoods"
- >添加商品</el-button
- <el-table height="100%" class="sa-table" :data="goods.data" stripe>
- <el-table-column prop="id" label="ID" min-width="80"></el-table-column>
- <el-table-column label="商品来源" min-width="120" align="center">
- <div class="sa-table-line-1">{{ scope.row.type_text }}</div>
- <el-table-column label="商品封面" min-width="80" align="center">
- <sa-preview :url="scope.row.cover_img_url" size="30"></sa-preview>
- <el-table-column label="商品名称" min-width="220" align="center">
- <div class="sa-table-line-1">{{ scope.row.name }}</div>
- <el-table-column label="价格形式" min-width="120" align="center">
- <div class="sa-table-line-1">{{ scope.row.price_type_text }}</div>
- <el-table-column label="价格" min-width="200" align="center">
- <div class="sa-table-line-1">
- <div :class="{ 'price-title': scope.row.price_type === 3 }"
- >{{ scope.row.price }}元</div
- {{ formatPrice(scope.row.price2, scope.row.price_type) }}
- <el-table-column label="商品路径" min-width="300" align="center">
- <div class="sa-table-line-1">{{ scope.row.url }}</div>
- <el-table-column label="审核状态" min-width="120" align="center" fixed="right">
- class="sa-table-line-1"
- scope.row.audit_status === 2
- ? 'sa-color--success'
- : scope.row.audit_status === 3
- ? 'sa-color--danger'
- : scope.row.audit_status === 1
- ? 'sa-color--warning'
- : 'sa-color--info'
- >{{ scope.row.audit_status_text }}</div
- class="is-link"
- icon="RefreshRight"
- @click="getStatus(scope.row.id)"
- ></el-button>
- <el-table-column label="操作" min-width="200" fixed="right">
- v-auth="'shop.admin.app.mplive.goods.detail'"
- @click="editGoods(scope.row.id)"
- >编辑</el-button
- v-auth="'shop.admin.app.mplive.goods.audit'"
- @click="check(scope.row.id, 'resubmit')"
- v-if="scope.row.audit_status === 0 || scope.row.audit_status === 3"
- >提交审核</el-button
- @click="check(scope.row.id, 'reset')"
- v-if="scope.row.audit_status === 1"
- >撤回审核</el-button
- <el-popconfirm
- width="fit-content"
- confirm-button-text="确认"
- cancel-button-text="取消"
- title="确认删除这条记录?"
- @confirm="deleteGoods(scope.row.id)"
- v-auth="'shop.admin.app.mplive.goods.delete'"
- type="danger"
- </el-popconfirm>
- name: 'shop.admin.mplive.goods',
- import GoodsEdit from './edit.vue';
- function formatPrice(price, type) {
- if (type === 1) {
- return '';
- } else if (type === 2) {
- return '~' + price + '元';
- return price + '元';
- name: {
- type: 'tinput',
- field: 'name',
- label: '商品名称',
- placeholder: '请输入商品名称',
- const { openFilter, deleteFilter } = useSearch({ filterParams, getData });
- //商品库表格
- // 获取商品库数据
- name: 'like',
- const { error, data } = await api.mplive.goods.list({
- order: goods.order,
- sort: goods.sort,
- goods.data = data.data;
- //添加商品
- function addGoods() {
- GoodsEdit,
- title: '添加商品',
- type: 'add',
- width: '80%',
- //编辑商品
- function editGoods(id) {
- title: '编辑商品库',
- id: id,
- // 删除商品
- async function deleteGoods(id) {
- await api.mplive.goods.delete(id);
- //刷新状态
- async function getStatus(id) {
- const { error } = await api.mplive.goods.status(id);
- //审核
- async function check(id, act) {
- const { error } = await api.mplive.goods.audit(id, {
- act,
- .price-title {
- text-decoration: line-through;
- margin-right: 8px;
- color: var(--sa-subfont);
- .live-qrcode {
- width: 150px;
- height: 150px;
@@ -1,34 +0,0 @@
- <el-tabs class="sa-tabs" v-model="dispatchType">
- <el-tab-pane label="直播间管理" name="live"></el-tab-pane>
- <el-tab-pane label="商品库管理" name="goods"></el-tab-pane>
- <room v-if="dispatchType === 'live'"></room>
- <goods v-if="dispatchType === 'goods'"></goods>
- name: 'shop.admin.mplive',
- import {ref} from 'vue';
- import room from './room/index.vue';
- import goods from './goods/index.vue';
- const dispatchType = ref('live');
@@ -1,302 +0,0 @@
- <el-container v-loading="loading">
- <el-form-item label="直播类型" prop="type">
- <el-radio :label="0">手机直播</el-radio>
- <el-radio :label="1">推流设备直播</el-radio>
- <div class="desc" v-if="form.model.type === 0">通过“小程序直播”小程序开播</div>
- <div class="desc" v-if="form.model.type === 1"
- >通过第三方推流设备发起直播,请自行定义画面宽高比</div
- <!-- <el-form-item label="播放方式" prop="way">
- <el-radio-group v-model="form.model.way">
- <el-radio label="column">竖屏</el-radio>
- <el-radio label="row">横屏</el-radio>
- <div class="desc"
- >横屏:视频宽高比为16:9、4:3、1.85:1 ;竖屏:视频宽高比为9:16、2:3</div
- <el-form-item label="直播间标题" prop="name">
- <el-input v-model="form.model.name" placeholder="请输入直播间标题"></el-input>
- <el-form-item label="背景图" prop="cover_img">
- <sa-uploader
- v-model="form.model.cover_img"
- fileType="image"
- :filesize="2048"
- ></sa-uploader>
- <div class="desc sa-m-l-12"> 直播间背景图,图片建议像素1080*1920,大小不超过2M </div>
- <el-form-item label="分享图" prop="share_img">
- v-model="form.model.share_img"
- :filesize="1024"
- <div class="desc sa-m-l-12"> 直播间分享图,图片建议像素800*640,大小不超过1M </div>
- <el-form-item label="封面图" prop="feeds_img">
- v-model="form.model.feeds_img"
- :filesize="100"
- <div class="desc sa-m-l-12">
- 购物直播频道封面图,图片建议像素800*800,大小不超过100KB
- <el-form-item label="开播时间" prop="date_time">
- <el-date-picker
- v-model="form.model.date_time"
- type="datetimerange"
- value-format="YYYY-MM-DD HH:mm:ss"
- format="YYYY-MM-DD HH:mm:ss"
- :default-time="defaultTime"
- range-separator="至"
- start-placeholder="开始时间"
- end-placeholder="结束时间"
- prefix-icon="Calendar"
- :editable="false"
- :disabled-date="disabledDate"
- 开播时间需要在当前时间的30分钟后 并且 开始时间不能在 6 个月后<br />
- 开播时间和结束时间间隔不得短于30分钟,不得超过72小时<br />
- 直播间在开始时间前也可以开播,并且到结束时间后不会强制结束。<br />
- 若到结束时间仍未开播,则直播间无法再开播。
- <el-form-item label="主播昵称" prop="anchor_name">
- <el-input v-model="form.model.anchor_name" placeholder="请输入主播昵称"></el-input>
- <el-form-item label="主播微信账号" prop="anchor_wechat">
- v-model="form.model.anchor_wechat"
- placeholder="请输入主播微信账号"
- 每个直播间需要绑定一个用作核实主播身份,不会展示给观众。<br />
- 主播微信号,如果未实名认证,需要先前往“小程序直播”小程序进行实名验证。
- <el-popover :width="180" trigger="click">
- <el-button class="is-link" type="primary">小程序认证</el-button>
- <img class="qrcode-img" src="/static/images/wechatMplive/live-qrcode.png" />
- <el-form-item label="主播副号" prop="sub_anchor_wechat">
- <el-input v-model="form.model.sub_anchor_wechat" placeholder="请输入主播副号"></el-input>
- <el-form-item label="官方收录">
- v-model="form.model.is_feeds_public"
- 开启后本场直播将有可能被官方推荐。<br />
- 此项设置在直播间创建完成后可以在控制台内修改。
- <el-form-item label="允许点赞">
- <el-radio-group v-model="form.model.close_like">
- <el-radio :label="0">开启</el-radio>
- <el-radio :label="1">关闭</el-radio>
- <el-form-item label="展示商品货架">
- <el-radio-group v-model="form.model.close_goods">
- <el-form-item label="允许评论">
- <el-radio-group v-model="form.model.close_comment">
- <el-form-item label="允许回放">
- <el-radio-group v-model="form.model.close_replay">
- <el-form-item label="打开客服">
- <el-radio-group v-model="form.model.close_kf">
- v-auth="'shop.admin.app.mplive.room.add'"
- v-auth="'shop.admin.app.mplive.room.edit'"
- import dayjs from 'dayjs';
- way: 'column',
- date_time: '',
- anchor_name: '',
- anchor_wechat: '',
- sub_anchor_wechat: '',
- is_feeds_public: 1,
- close_kf: 0,
- close_replay: 0,
- close_comment: 0,
- close_goods: 0,
- close_like: 0,
- feeds_img: '',
- share_img: '',
- cover_img: '',
- type: [{ required: true, message: '请选择直播类型', trigger: 'change' }],
- way: [{ required: true, message: '请选择播放方式', trigger: 'change' }],
- feeds_img: [{ required: true, message: '请选择封面图', trigger: 'change' }],
- share_img: [{ required: true, message: '请选择分享图', trigger: 'change' }],
- cover_img: [{ required: true, message: '请选择背景图', trigger: 'change' }],
- date_time: [{ required: true, message: '请选择开播时间', trigger: 'change' }],
- anchor_name: [{ validator: checkNickname, trigger: 'change' }],
- anchor_wechat: [{ required: true, message: '请输入主播微信账号', trigger: 'blur' }],
- //获取默认开始时间为当前时间后40分钟
- const defaultTime = ref([
- new Date(new Date().getTime() + 40 * 60 * 1000),
- new Date(2000, 2, 1, 23, 59, 59),
- ]);
- const { error, data } = await api.mplive.room.detail(id);
- form.model.date_time = [
- dayjs(data.start_time * 1000).format('YYYY-MM-DD HH:mm:ss'),
- dayjs(data.end_time * 1000).format('YYYY-MM-DD HH:mm:ss'),
- // 禁止时间
- function disabledDate(time) {
- return time.getTime() < Date.now() - 86400000;
- submitForm.start_time = Number(new Date(form.model.date_time[0]).getTime() / 1000);
- submitForm.end_time = Number(new Date(form.model.date_time[1]).getTime() / 1000);
- delete submitForm.date_time;
- ? await api.mplive.room.add(submitForm)
- : await api.mplive.room.edit(props.modal.params.id, submitForm);
- return callback(new Error('请输入直播间标题'));
- if (length < 6 || length > 34) {
- callback(new Error('直播标题必须为3-17个字(一个字等于两个英文字符或特殊字符)'));
- function checkNickname(rule, value, callback) {
- return callback(new Error('请输入主播昵称'));
- if (length < 4 || length > 30) {
- callback(new Error('直播标题必须为2-15个字(一个字等于两个英文字符或特殊字符)'));
- font-weight: normal;
- .qrcode-img {
@@ -1,322 +0,0 @@
- <span class="left">直播间管理</span>
- <el-popover placement="bottom" :width="174" trigger="click">
- <el-button class="sa-button-refresh">主播端</el-button>
- <img class="live-qrcode" src="/static/images/wechatMplive/live-qrcode.png" />
- v-auth="'shop.admin.app.mplive.room.sync'"
- @click="sync"
- :loading="loading"
- >同步直播间</el-button
- @click="addRow"
- >创建直播间</el-button
- <el-table height="100%" class="sa-table" :data="live.data" stripe @sort-change="fieldFilter">
- <el-table-column label="房间ID" min-width="120" align="center" sortable="custom">
- <div class="sa-table-line-1">{{ scope.row.roomid }}</div>
- <el-table-column label="直播类型" min-width="120" align="center">
- <el-table-column label="直播间标题" min-width="200" align="center">
- <el-table-column label="主播昵称" min-width="120" align="center">
- <div class="sa-table-line-1">{{ scope.row.anchor_name }}</div>
- <el-table-column label="状态" min-width="120" align="center">
- scope.row.status === 101
- : scope.row.status === 102
- : scope.row.status === 105
- ? 'sa-color--info'
- : 'sa-color--danger'
- >{{ scope.row.status_text }}</div
- <el-table-column label="背景图" min-width="80" align="center">
- <sa-preview :url="scope.row.cover_img" size="30"></sa-preview>
- <el-table-column label="分享图" min-width="80" align="center">
- <sa-preview :url="scope.row.share_img" size="30"></sa-preview>
- <el-table-column label="封面图" min-width="80" align="center">
- <sa-preview :url="scope.row.feeds_img" size="30"></sa-preview>
- <el-table-column label="开播时间" min-width="200" align="center">
- <div class="sa-table-line-1">{{
- dayjs(scope.row.start_time * 1000).format('YYYY-MM-DD HH:mm')
- <el-table-column label="结束时间" min-width="200" align="center">
- dayjs(scope.row.end_time * 1000).format('YYYY-MM-DD HH:mm')
- <el-table-column label="操作" min-width="340" fixed="right" align="center">
- v-auth="'shop.admin.app.mplive.room.detail'"
- @click="editRow(scope.row.roomid)"
- v-auth="'shop.admin.app.mplive.room.pushUrl'"
- @click="pushUrl(scope.row.roomid)"
- v-if="scope.row.type === 1 && (scope.row.status === 101 || scope.row.status === 102)"
- >推流地址</el-button
- v-auth="'shop.admin.app.mplive.room.qrcode'"
- @click="shareQrcode(scope.row.roomid)"
- v-if="scope.row.status === 101 || scope.row.status === 102"
- >分享二维码</el-button
- v-auth="'shop.admin.app.mplive.room.playback'"
- @click="playBack(scope.row.roomid)"
- v-if="scope.row.status === 103"
- >回放</el-button
- @confirm="deleteApi(scope.row.roomid)"
- v-auth="'shop.admin.app.mplive.room.delete'"
- name: 'shop.admin.mplive.room',
- import liveEdit from './edit.vue';
- import PushUrl from './pushUrl.vue';
- import ShareQrcode from './qrcode.vue';
- import PlayBack from './playback.vue';
- // 直播间表格
- const live = reactive({
- // 获取直播间数据
- async function getLiveData() {
- const { error, data } = await api.mplive.room.list({
- order: live.order,
- sort: live.sort,
- live.data = data;
- // table 字段排序
- function fieldFilter({ prop, order }) {
- live.order = order == 'ascending' ? 'asc' : 'desc';
- live.sort = prop;
- getLiveData();
- //添加直播间
- function addRow() {
- liveEdit,
- title: '添加直播间',
- //编辑直播间
- function editRow(id) {
- title: '编辑直播间',
- // 删除直播间
- async function deleteApi(id) {
- await api.mplive.room.delete(id);
- //推流地址
- async function pushUrl(id) {
- PushUrl,
- title: '推流地址',
- //分享二维码
- async function shareQrcode(id) {
- ShareQrcode,
- title: '分享二维码',
- //同步直播间
- async function sync() {
- const { error, data } = await api.mplive.room.sync();
- //直播回放
- async function playBack(id) {
- PlayBack,
- title: '直播回放列表',
- width: 144px;
- height: 144px;
@@ -1,118 +0,0 @@
- <el-table :data="table.data" class="sa-table" stripe>
- <el-table-column label="回放片段" min-width="140">
- <div class="sa-table-line-1">{{ '片段' + scope.row.index || '-' }}</div>
- <el-table-column label="创建时间" min-width="140">
- dayjs(scope.row.create_time).format('YYYY-MM-DD HH:mm:ss') || '-'
- <el-table-column label="过期时间" min-width="140">
- dayjs(scope.row.expire_time).format('YYYY-MM-DD HH:mm:ss') || '-'
- <el-table-column fixed="right" label="操作" min-width="120">
- <el-button class="is-link" type="primary" @click="play(scope.row.media_url)"
- >播放</el-button
- <!-- <sa-view-bar>
- </sa-view-bar> -->
- <script setup>
- import { usePagination } from '@/sheep/hooks';
- // 表格状态
- async function getData(id) {
- const { error, data } = await api.mplive.playback(id, {
- table.data = data;
- table.data.forEach((item, index) => {
- table.data[index].index = index + 1;
- function play(url) {
- window.open(url);
- await getData(props.modal.params.id);
- </script>
- <style lang="scss" scoped>
- .program {
- width: 360px;
- height: 260px;
- background: #f5f5f5;
- .path {
- .title {
- color: var(--sa-subtitle);
- word-break: break-all;
- </style>
@@ -1,85 +0,0 @@
- <div class="title sa-m-b-16">推流地址</div>
- <div class="sa-flex sa-m-b-4">
- <div class="desc sa-m-r-4">在第三方推流应用中,以下地址进行推流</div>
- <el-button class="is-link" type="primary" @click="onCopy">复制链接</el-button>
- <div class="desc sa-m-b-16">{{ state.pushUrl }}</div>
- <div class="subtitle sa-m-b-40">此地址为当前直播间唯一推流地址,不要泄露给第三方。</div>
- <div class="title sa-m-b-16">备注</div>
- <div class="desc sa-m-b-4">服务器地址:{{ state.serverAddress }}</div>
- <div class="desc sa-m-b-16">串流密钥:{{ state.key }}</div>
- <div class="subtitle sa-m-r-4">推流直播操作详见</div>
- <el-button class="is-link" type="primary" @click="onJump">指引</el-button>
- pushUrl: '', // 推流地址
- serverAddress: '', // 服务器地址
- key: '', // 串流密钥
- const { error, data } = await api.mplive.room.pushUrl(id);
- state.pushUrl = data.pushAddr;
- state.serverAddress = state.pushUrl.split('/live/')[0] + '/live/';
- state.key = state.pushUrl.split('/live/')[1];
- //复制推流地址
- handleClipboard(state.pushUrl);
- function onJump() {
- window.open('https://docs.qq.com/doc/DV0hoWHZRdm9oT2pp');
- color: var(--sa-title);
- .subtitle {
@@ -1,102 +0,0 @@
- <div class="sa-flex sa-flex-wrap sa-row-between">
- <div class="program sa-flex sa-col-top sa-row-between sa-p-16 sa-m-b-30">
- <div class="title sa-m-b-8">直播间小程序码</div>
- <div class="desc sa-m-b-24">小程序码不带参数</div>
- <el-button type="primary" @click="saveImg">保存图片</el-button>
- <sa-image class="" :url="state.cdnUrl" size="120"></sa-image>
- <div class="path sa-p-16 sa-m-b-30">
- <div class="title sa-m-b-16">直播间页面路径</div>
- <div class="desc sa-m-b-24">{{ state.path }}</div>
- <el-button type="primary" @click="onCopy">复制链接</el-button>
- <div class="desc sa-m-t-40">链接是直播间原始页面路径,如需加入参数,详见<el-button class="is-link" type="primary"
- @click="onJump">使用方法</el-button></div>
- cdnUrl: '', // 小程序码
- path: '', // 页面路径
- const { error, data } = await api.mplive.room.qrcode(id);
- state.cdnUrl = data.cdnUrl;
- state.path = data.pagePath;
- handleClipboard(state.path);
- function saveImg() {
- window.open(state.cdnUrl);
- window.open(
- 'https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/liveplayer/live-player-plugin.html',
@@ -1,126 +0,0 @@
- <el-main v-loading="loading">
- <el-table height="100%" class="sa-table" :data="state.data" stripe @selection-change="onSelectionChange">
- <el-table-column type="selection" :selectable="isSelectable" width="48"></el-table-column>
- <el-table-column label="房间ID" min-width="120" align="center">
- <div class="sa-table-line-1" :class="
- ">{{ scope.row.status_text }}</div>
- <el-button type="primary" @click="onConfirm">确定</el-button>
-import { onMounted, reactive, ref } from 'vue';
-import { api } from '../app.service';
-import dayjs from 'dayjs';
-const emit = defineEmits(['modalCallBack']);
-const props = defineProps(['modal']);
-const loading = ref(true);
-const state = reactive({
-});
-async function getData() {
- const { error, data } = await api.mplive.room.select();
- state.data = data;
-}
-function isSelectable(row) {
- return row.status === 101 || row.status === 102 || row.status === 103
-function onSelectionChange(val) {
- state.selected = val
-function onConfirm() {
- emit('modalCallBack', {
- event: 'confirm',
- data: state.selected,
-onMounted(() => {
@@ -1,196 +0,0 @@
- <el-button v-if="isEmpty(goods.data) && modal.params.type == 'add'" @click="selectGoods"
- <template v-if="!isEmpty(goods.data)">
- <div class="title sa-m-b-20">商品名称:{{ goods.data.title }}</div>
- <div class="sku-table-wrap">
- <table class="sku-table" rules="none">
- <thead>
- <tr>
- <th class="sku-item" v-for="ss in goods.skus" :key="ss">
- {{ ss.name }}
- </th>
- <th class="sku-item">是否参与</th>
- <th class="sku-item">商品价格</th>
- <th class="sku-item">可兑换数量</th>
- <th class="sku-item">兑换积分</th>
- <th class="sku-item">兑换价格</th>
- </tr>
- </thead>
- <tbody>
- <tr v-for="(sp, spindex) in goods.skuPrice" :key="sp">
- <td class="sku-item" v-for="st in sp.goods_sku_text" :key="st">
- {{ st }}
- </td>
- <td class="sku-item">
- v-model="goods.score_sku_prices[spindex].status"
- active-value="up"
- inactive-value="down"
- <td class="sku-item">{{ sp.price }}</td>
- <th class="sku-item">
- v-if="goods.score_sku_prices[spindex].status == 'up'"
- v-model="goods.score_sku_prices[spindex].stock"
- v-model="goods.score_sku_prices[spindex].score"
- v-model="goods.score_sku_prices[spindex].price"
- </tbody>
- </table>
- v-auth="'shop.admin.app.scoreshop.add'"
- @click="modelBack"
- v-auth="'shop.admin.app.scoreshop.edit'"
- import { onMounted, reactive } from 'vue';
- import { api } from '../app.service';
- import { isEmpty } from 'lodash';
- import { ElMessage } from 'element-plus';
- import GoodsSelect from '../../goods/goods/select.vue';
- ftype: 'score_shop',
- goods.data = res.data;
- getSkus(res.data.id);
- data: props.modal.params.data || {},
- skus: [],
- skuPrice: [],
- score_sku_prices: [],
- async function getSkus(id) {
- const { data } = await api.scoreShop.skus(id);
- goods.skus = data.skus;
- goods.skuPrice = data.sku_prices;
- goods.score_sku_prices = data.score_sku_prices;
- async function modelBack() {
- let flag = true;
- goods.score_sku_prices.forEach((s) => {
- if (s.status == 'up' && s.score <= 0) {
- flag = false;
- if (!flag) {
- ElMessage({
- message: '兑换积分必须大于0',
- type: 'warning',
- ? await api.scoreShop.add({
- goods_id: goods.data.id,
- sku_prices: goods.score_sku_prices,
- })
- : await api.scoreShop.edit(goods.data.id, {
- if (props.modal.params.type == 'edit') {
- getSkus(props.modal.params.data.id);
- line-height: 22px;
- .sku-table-wrap {
- overflow: auto;
- .sku-table {
- thead {
- line-height: 40px;
- background: var(--sa-table-header-bg);
- color: var(--subtitle);
- tbody {
- tr {
- line-height: 48px;
- &:nth-of-type(2n) {
- background: var(--sa-table-striped);
- th,
- td {
- padding: 0 16px;
- text-align: left;
- .sku-item {
- min-width: 100px;
- flex-shrink: 0;
@@ -1,340 +0,0 @@
- <el-container class="scoreshop-view panel-block">
- 积分商城
- >新建</el-button
- v-auth="'shop.admin.app.scoreshop.recyclebin'"
- icon="Delete"
- @click="openRecyclebin"
- >回收站</el-button
- <el-main class="sa-p-0">
- <div class="sa-table-wrap panel-block panel-block--bottom" v-loading="loading">
- <el-table
- height="100%"
- class="sa-table"
- :data="table.data"
- :expand-row-keys="expandRowKeys"
- row-key="id"
- stripe
- <el-table-column type="expand">
- <template #default="props">
- <el-table class="sa-table sa-expand-table" :data="skuPrices.data" stripe>
- <el-table-column width="48"></el-table-column>
- <el-table-column min-width="80"></el-table-column>
- <el-table-column min-width="440">
- <sa-preview :url="scope.row.image || props.row.image" size="32"></sa-preview>
- <div class="sku-text sa-m-l-12">
- {{ scope.row.goods_sku_text?.join('/') }}
- <el-table-column prop="score_price" min-width="200"></el-table-column>
- <el-table-column prop="sales" min-width="100"></el-table-column>
- <el-table-column prop="stock" min-width="100"></el-table-column>
- <el-table-column min-width="120"></el-table-column>
- <el-table-column label="商品信息" min-width="440">
- <goods-item
- :goods="{
- image: scope.row.image,
- title: scope.row.title,
- subtitle: scope.row.subtitle,
- }"
- mode="scoreShop"
- <template #sku>
- v-if="scope.row.is_sku"
- v-auth="'shop.admin.app.scoreshop.skuprices'"
- link
- <div class="sku" @click.stop="expandRow(scope.row.id)">
- <span>多规格</span>
- <el-icon
- :class="[
- 'expand-arrow sa-m-l-4',
- expandRowKeys.includes(scope.row.id)
- ? 'expand-arrow-up'
- : 'expand-arrow-down',
- ]"
- <ArrowDown />
- </goods-item>
- <el-table-column label="积分现金" min-width="200">
- <div v-if="scope.row.score_price" class="sa-flex">
- {{ scope.row.score_price.score }}积分
- <div v-if="Number(scope.row.score_price.price)"
- >+¥{{ scope.row.score_price.price }}</div
- <div v-else>-</div>
- <el-table-column prop="score_sales" label="销量" min-width="100"></el-table-column>
- <el-table-column prop="score_stock" label="库存" min-width="100"></el-table-column>
- <el-table-column label="操作" min-width="120" fixed="right">
- v-auth="'shop.admin.app.scoreshop.skus'"
- @click="editRow(scope.row)"
- @confirm="deleteRow(scope.row.id)"
- v-auth="'shop.admin.app.scoreshop.delete'"
- import GoodsItem from '@/app/shop/components/goods-item.vue';
- import ScoreShopEdit from './edit.vue';
- import ScoreShopRecyclebin from './recyclebin.vue';
- label: '请输入查询内容',
- field: 'title',
- value: 'title',
- label: '商品副标题',
- value: 'subtitle',
- keyword: { field: 'title', value: '' },
- // 表格
- // 获取数据
- title: 'like',
- subtitle: 'like',
- const { error, data } = await api.scoreShop.list({
- if (expandRowKeys.length > 0) {
- getSkuPrices(expandRowKeys[0]);
- ScoreShopEdit,
- title: '兑换规则',
- function editRow(row) {
- id: row.id,
- title: row.title,
- async function deleteRow(id) {
- await api.scoreShop.delete(id);
- function openRecyclebin() {
- ScoreShopRecyclebin,
- title: '回收站',
- let expandRowKeys = reactive([]);
- function expandRow(id) {
- if (expandRowKeys.includes(id)) {
- expandRowKeys.length = 0;
- expandRowKeys.push(id);
- getSkuPrices(id);
- const skuPrices = reactive({
- async function getSkuPrices(id) {
- const { data } = await api.scoreShop.getSkuPrices(id);
- skuPrices.data = data;
- .scoreshop-view {
- margin-left: -48px;
- .sku {
- width: fit-content;
- height: 20px;
- line-height: 1;
- padding: 0 8px;
- border-radius: 10px;
- cursor: pointer;
- .sa-expand-table {
- .el-table__header-wrapper {
- .sku-text {
@@ -1,184 +0,0 @@
- <el-container class="recyclebin-view">
- @selection-change="changeSelection"
- @sort-change="fieldFilter"
- <el-table-column type="selection" width="48" align="center"></el-table-column>
- <el-table-column sortable="custom" prop="id" label="ID" min-width="100"></el-table-column>
- <el-table-column label="名称" min-width="100">
- <div class="sa-table-line-1">{{ scope.row.title || '-' }}</div>
- <el-table-column
- sortable="custom"
- prop="delete_time"
- label="删除时间"
- min-width="172"
- ></el-table-column>
- v-auth="'shop.admin.app.scoreshop.restore'"
- @click="restoreRow(scope.row.id)"
- >还原</el-button
- title="确认销毁这条记录?"
- @confirm="destroyRow(scope.row.id)"
- <el-button v-auth="'shop.admin.app.scoreshop.destroy'" class="is-link" type="danger"
- >销毁</el-button
- <template #left>
- <sa-batch-handle
- :batchHandleTools="batchHandleTools"
- :selectedLeng="table.selected.length"
- @batchHandle="batchHandle"
- ></sa-batch-handle>
- import { ElMessageBox } from 'element-plus';
- const { data } = await api.scoreShop.recyclebin({
- table.order = order == 'ascending' ? 'asc' : 'desc';
- table.sort = prop;
- // table 批量选择
- function changeSelection(row) {
- table.selected = row;
- // 批量操作
- const batchHandleTools = [
- type: 'restore',
- label: '还原',
- auth: 'shop.admin.app.scoreshop.restore',
- buttonType: 'primary',
- type: 'destroy',
- label: '销毁',
- auth: 'shop.admin.app.scoreshop.destroy',
- buttonType: 'danger',
- type: 'all',
- label: '清空回收站',
- operType: 'all',
- async function batchHandle(type) {
- let ids = [];
- table.selected.forEach((row) => {
- ids.push(row.id);
- switch (type) {
- case 'all':
- ElMessageBox.confirm('此操作将清空回收站, 是否继续?', '提示', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- }).then(() => {
- destroyRow('all');
- break;
- case 'destroy':
- ElMessageBox.confirm('此操作将销毁, 是否继续?', '提示', {
- destroyRow(ids.join(','));
- case 'restore':
- ElMessageBox.confirm('此操作将还原, 是否继续?', '提示', {
- restoreRow(ids.join(','));
- // 销毁
- async function destroyRow(id) {
- await api.scoreShop.destroy(id);
- // 还原
- async function restoreRow(id) {
- await api.scoreShop.restore(id);
@@ -1,120 +0,0 @@
- @selection-change="handleSelectionChange"
- <el-table-column v-if="modal.params.multiple" type="selection" width="55" />
- <div class="sku" v-if="scope.row.is_sku">
- <el-table-column v-if="!modal.params.multiple" label="操作" width="80">
- <el-button class="is-link" type="primary" @click="onSingleSelect(scope.row)"
- >选择</el-button
- <el-footer class="sa-flex sa-row-between">
- <el-button v-if="modal.params.multiple" type="primary" @click="modalCallBack">确定</el-button>
- name: 'ScoreShopSelect',
- const { data } = await api.scoreShop.select({
- function handleSelectionChange(val) {
- table.selected = val;
- function modalCallBack() {
- emit('modalCallBack', { event: 'confirm', data: table.selected });
- function onSingleSelect(row) {
- data: row,
@@ -1,17 +0,0 @@
-import { SELECT, CRUD } from '@/sheep/request/crud';
- path: 'category',
- name: 'shop.admin.category',
- component: () => import('@/app/shop/admin/category/index.vue'),
- title: '商品分类',
- ...CRUD('shop/admin/category_tag'),
- select: (params) => SELECT('shop/admin/category_tag', params),
@@ -1,129 +0,0 @@
- <el-container class="category-select-view">
- <div class="label sa-flex">选择分类</div>
- <el-button @click="handleCancel">取消</el-button>
- <el-button type="primary" @click="handleConfirm">确定</el-button>
- <div class="category-list">
- <el-tree
- ref="treeRef"
- :data="categoryList"
- :props="treeProps"
- node-key="id"
- :default-checked-keys="selectedIds"
- show-checkbox
- :check-strictly="checkStrictly"
- @check="handleCheck"
- name: 'CategorySelect',
-import { ref, reactive, onMounted } from 'vue';
-import { api } from './category.service';
-const props = defineProps({
- default: () => ({}),
- multiple: {
- type: Boolean,
- default: true,
- checkStrictly: {
- default: false,
-const treeRef = ref();
-const categoryList = ref([]);
-const selectedIds = ref([]);
-const treeProps = {
- children: 'children',
- label: 'name',
-// 获取分类数据
- try {
- const { data, error } = await api.category.list();
- categoryList.value = data || [];
- } catch (error) {
- console.error('获取分类数据失败:', error);
-// 处理选择
-function handleCheck(data, checked) {
- if (props.multiple) {
- selectedIds.value = checked.checkedKeys;
- selectedIds.value = [data.id];
- // 单选模式下,取消其他选择
- treeRef.value.setCheckedKeys([data.id]);
-// 确认选择
-function handleConfirm() {
- const checkedNodes = treeRef.value.getCheckedNodes();
- const selectedCategories = checkedNodes.map(node => ({
- id: node.id,
- name: node.name,
- }));
- type: 'confirm',
- data: props.multiple ? selectedCategories : selectedCategories[0] || null,
-// 取消选择
-function handleCancel() {
- type: 'cancel',
- // 设置默认选中
- if (props.modal.selectedIds) {
- selectedIds.value = Array.isArray(props.modal.selectedIds)
- ? props.modal.selectedIds
- : [props.modal.selectedIds];
-.category-select-view {
- .category-list {
- padding: 20px;
- height: 400px;
- overflow-y: auto;
@@ -7,8 +7,7 @@
</el-form-item>
<el-form-item label="支付方式类型">
- <el-radio-group v-model="form.model.type" :disabled="modal.params.type=='edit'">
- <el-radio label="wechat"> 微信支付V3版 </el-radio>
+ <el-radio-group v-model="form.model.type" :disabled="modal.params.type == 'edit'">
<el-radio label="alipay">支付宝支付</el-radio>
</el-radio-group>
@@ -216,7 +215,7 @@
sub_mch_public_cert_path: '',
sub_mch_secret_cert: '',
- type: 'wechat',
+ type: 'alipay',
rules: {
name: [{ required: true, message: '请输入标题', trigger: 'blur' }],
- path: 'banner',
- name: 'shop.admin.content.banner',
- component: () => import('@/app/shop/admin/content/banner/index.vue'),
- title: '广告位',
- ...CRUD('shop/admin/banner'),
- select: (params) => SELECT('shop/admin/banner', params),
@@ -65,7 +65,7 @@
<script setup>
import { cloneDeep } from 'lodash';
import { onMounted, reactive, ref, unref } from 'vue';
- import { api } from './banner.service';
+ import { api } from '../content.service';
const emit = defineEmits(['modalCallBack']);
const props = defineProps({
modal: {
@@ -46,9 +46,9 @@
</el-table-column>
<el-table-column label="广告图片" min-width="120">
<template #default="scope">
- <el-image
+ <el-image
v-if="scope.row.image"
- :src="scope.row.image"
+ :src="scope.row.image"
style="width: 60px; height: 40px"
fit="cover"
/>
@@ -121,7 +121,7 @@
</template>
import { onMounted, reactive, ref } from 'vue';
import { ElMessageBox } from 'element-plus';
import { useModal } from '@/sheep/hooks';
import { usePagination } from '@/sheep/hooks';
@@ -0,0 +1,59 @@
+import Content from '@/sheep/layouts/content.vue';
+import { SELECT, CRUD } from '@/sheep/request/crud';
+const route = {
+ path: 'content',
+ name: 'shop.admin.content',
+ component: Content,
+ meta: {
+ title: '内容',
+ children: [
+ {
+ path: 'banner',
+ name: 'shop.admin.content.banner',
+ component: () => import('./banner/index.vue'),
+ title: '广告位',
+ path: 'notification',
+ name: 'shop.admin.content.notification',
+ component: () => import('./notification/index.vue'),
+ title: '消息推送',
+ path: 'sms',
+ name: 'shop.admin.content.sms',
+ component: () => import('./sms/index.vue'),
+ title: '短信',
+ ],
+};
+const api = {
+ // 广告位相关 API
+ banner: {
+ ...CRUD('shop/admin/banner'),
+ select: (params) => SELECT('shop/admin/banner', params),
+ // 消息推送相关 API
+ notification: {
+ ...CRUD('shop/admin/notification'),
+ select: (params) => SELECT('shop/admin/notification', params),
+ // 短信相关 API
+ sms: {
+ ...CRUD('shop/admin/sms_template'),
+ select: (params) => SELECT('shop/admin/sms_template', params),
+export { route, api };
@@ -20,9 +20,9 @@
</el-select>
<el-form-item label="消息内容" prop="content">
- v-model="form.model.content"
+ <el-input
+ v-model="form.model.content"
+ type="textarea"
:rows="5"
placeholder="请填写消息内容"
></el-input>
@@ -54,7 +54,7 @@
- import { api } from './notification.service';
@@ -108,7 +108,7 @@
- path: 'notification',
- name: 'shop.admin.content.notification',
- component: () => import('@/app/shop/admin/content/notification/index.vue'),
- title: '消息推送',
- ...CRUD('shop/admin/notification'),
- select: (params) => SELECT('shop/admin/notification', params),
@@ -16,9 +16,9 @@
<el-form-item label="模板内容" prop="content">
:rows="4"
placeholder="请填写短信模板内容,使用{变量名}表示变量"
@@ -27,9 +27,9 @@
</div>
<el-form-item label="变量说明" prop="variables">
- v-model="form.model.variables"
+ v-model="form.model.variables"
:rows="3"
placeholder="请说明模板中使用的变量,如:{code}=验证码,{name}=用户名"
- import { api } from './sms.service';
@@ -110,7 +110,7 @@
- path: 'sms',
- name: 'shop.admin.content.sms',
- component: () => import('@/app/shop/admin/content/sms/index.vue'),
- title: '短信',
- ...CRUD('shop/admin/sms_template'),
- select: (params) => SELECT('shop/admin/sms_template', params),
@@ -1,33 +0,0 @@
- path: 'coupon',
- name: 'shop.admin.coupon',
- component: () => import('./index.vue'),
- title: '优惠券',
- ...CRUD('shop/admin/coupon'),
- ...RECYCLE('shop/admin/coupon'),
- url: `shop/admin/coupon/select?type=${type}`,
- send: (id, data) =>
- url: `shop/admin/coupon/send/${id}`,
@@ -1,489 +0,0 @@
- <el-container class="coupon-edit">
- <el-form :model="form.model" :rules="form.rules" ref="formRef" label-width="110px">
- <el-form-item label="券名称" prop="name">
- <el-input v-model="form.model.name" placeholder="例如:国庆优惠券" />
- <el-form-item label="名称备注" prop="description">
- <el-input v-model="form.model.description" placeholder="请输入备注" />
- <el-form-item label="券类型" prop="type">
- <el-radio-group v-model="form.model.type" :disabled="props.modal.params.type == 'edit'">
- <el-radio label="reduce">满减券</el-radio>
- <el-radio label="discount">折扣券</el-radio>
- <el-form-item class="el-form-item--label-right">
- <el-form-item class="el-form-item__label-auto" label="消费满" prop="enough">
- <div class="sa-w-120">
- v-model="form.model.enough"
- :disabled="props.modal.params.type == 'edit'"
- v-if="form.model.type == 'reduce'"
- class="el-form-item__label-auto is-no-asterisk sa-m-l-16"
- label="立减"
- prop="amount"
- <div class="sa-w-113">
- v-model="form.model.amount"
- v-if="form.model.type == 'discount'"
- label="打"
- :max="10"
- <template #append>折</template>
- <el-form-item v-if="form.model.type == 'discount'" class="el-form-item--label-right">
- <el-form-item class="el-form-item__label-auto" label="最多优惠" prop="max_amount">
- <div class="enough-input">
- v-model="form.model.max_amount"
- class="sa-w-120"
- <el-form-item label="发券总量" prop="stock">
- <el-input type="number" v-model="form.model.stock" class="sa-w-120" :min="0">
- <template #append>张</template>
- <el-form-item label="每人限领次数">
- <el-input type="number" v-model="form.model.limit_num" class="sa-w-120" :min="0">
- <el-form-item label="领券时间" prop="get_time" class="get-time">
- v-model="form.model.get_time"
- :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 2, 1, 23, 59, 59)]"
- start-placeholder="开始日期"
- end-placeholder="结束日期"
- ></el-date-picker>
- <el-form-item label="券有效期" prop="use_time_type">
- <el-radio-group v-model="form.model.use_time_type">
- <el-radio label="days">相对天数</el-radio>
- <el-radio label="range">固定区间</el-radio>
- <template v-if="form.model.use_time_type == 'days'">
- class="el-form-item__label-auto is-no-asterisk"
- label="领券"
- prop="start_days"
- <el-input type="number" v-model="form.model.start_days" :min="0">
- <template #append>天</template>
- label="后生效,有效期"
- prop="days"
- <el-input type="number" v-model="form.model.days" :min="0">
- v-if="form.model.use_time_type == 'range'"
- label="固定时间"
- prop="useTime"
- v-model="form.model.use_time"
- <el-form-item label="优惠叠加">
- v-model="form.model.is_double_discount"
- class="sa-m-r-8"
- <div v-if="form.model.is_double_discount == '1'" class="openswitch"> 开启 </div>
- <div v-if="form.model.is_double_discount == '0'">关闭</div>
- <div class="warning-title"> 开启优惠叠加,优惠券将可以和活动一起使用 </div>
- <el-form-item label="券状态" prop="status">
- <el-radio-group v-model="form.model.status">
- <el-radio label="normal">公开发放</el-radio>
- <el-radio label="hidden">后台发放</el-radio>
- <el-radio label="disabled">禁止使用</el-radio>
- <div class="warning-title">
- 后台发放状态改为别的状态,将导致满赠活动无法赠送该优惠券
- <el-form-item label="可用范围" prop="use_scope">
- <el-radio-group v-model="form.model.use_scope" @change="changeUseScope">
- <el-radio label="all_use">全场通用</el-radio>
- <el-radio label="goods">指定商品可用</el-radio>
- <el-radio label="disabled_goods">指定商品不可用</el-radio>
- <el-radio label="category">指定分类可用</el-radio>
- <div v-if="form.model.use_scope == 'goods' || form.model.use_scope == 'disabled_goods'">
- <el-button class="is-link sa-m-b-8" type="primary" @click="selectGoods"
- <div class="sa-template-wrap" v-if="form.model.items_value.length > 0">
- <div class="key">商品信息</div>
- class="item"
- v-for="(element, index) in form.model.items_value"
- :key="element"
- <el-form-item class="key">
- <sa-image :url="element.image" size="40"></sa-image>
- <div class="goods sa-flex-1 sa-table-line-1 sa-m-l-12">
- <div class="goods-title sa-m-b-6">
- {{ element.title }}
- <el-button class="is-link" type="danger" @click="deleteItems(index)">
- 移除
- <div v-if="form.model.use_scope == 'category'">
- <el-button class="is-link sa-m-b-8" type="primary" @click="selectCategory"
- >选择分类</el-button
- <div class="key">分类信息</div>
- <div class="goods-title sa-m-b-6">{{ element.name }}</div>
- v-auth="'shop.admin.coupon.add'"
- v-auth="'shop.admin.coupon.edit'"
- import { isEmpty, cloneDeep } from 'lodash';
- import { api } from './coupon.service';
- import CategorySelect from '@/app/shop/admin/category/select.vue';
- const formRef = ref();
- type: 'reduce', // 优惠券类型:reduce=满减券,discount=折扣券
- use_scope: 'all_use', // 可用范围:all=全场通用,goods=指定商品可用,disabled_goods=指定商品不可用,category=指定分类可用
- items: '',
- items_value: [],
- amount: '',
- max_amount: '',
- enough: '',
- stock: '',
- limit_num: '',
- get_time: '',
- use_time_type: 'days',
- use_time: '',
- start_days: '',
- days: '',
- is_double_discount: '',
- description: '',
- status: 'normal', // 状态:normal=公开,hidden=后台发放,disabled=禁用
- name: [{ required: true, message: '请填写券名称', trigger: 'blur' }],
- type: [{ required: true, message: '请选择券类型', trigger: 'blur' }],
- enough: [{ required: true, message: '请填写消费门槛', trigger: 'blur' }],
- amount: [{ required: true, message: '请填写使用面额', trigger: 'blur' }],
- max_amount: [{ required: true, message: '请填写最大优惠', trigger: 'blur' }],
- stock: [{ required: true, message: '请填写发券总量', trigger: 'blur' }],
- get_time: [{ required: true, message: '请选择优惠券发放时间', trigger: 'blur' }],
- use_time: [{ required: true, message: '请选择优惠券可使用时间', trigger: 'blur' }],
- use_time_type: [{ required: true, message: '请选择优惠券使用时间类型', trigger: 'blur' }],
- days: [{ required: true, message: '请填写优惠券有效天数', trigger: 'blur' }],
- use_scope: [{ required: true, message: '请选择可用范围', trigger: 'blur' }],
- const { error, data } = await api.detail(props.modal.params.id);
- form.model.get_time = [data.get_start_time, data.get_end_time];
- if (data.use_time_type == 'days') {
- form.model.use_time = '';
- } else if (data.use_time_type == 'range') {
- form.model.use_time = [data.use_start_time, data.use_end_time];
- form.model.items_value.forEach((i) => {
- ids.push(i.id);
- multiple: true,
- ids,
- form.model.items_value = res.data;
- function selectCategory() {
- CategorySelect,
- from: 'coupon',
- form.model.items_value = [];
- form.model.items_value.push(...res.data.list);
- function deleteItems(index) {
- form.model.items_value.splice(index, 1);
- function changeUseScope() {
- if (Number(submitForm.enough) < Number(submitForm.amount)) {
- message: '请输入正确的使用门槛',
- return;
- if (
- submitForm.use_scope == 'goods' ||
- submitForm.use_scope == 'disabled_goods' ||
- submitForm.use_scope == 'category'
- ) {
- submitForm.items_value.forEach((i) => {
- submitForm.items = ids.join(',');
- } else if (submitForm.use_scope == 'all_use') {
- submitForm.items = '';
- delete submitForm.items_value;
- if (!isEmpty(submitForm.use_time)) {
- submitForm.use_time = submitForm.use_time.join(' - ');
- if (!isEmpty(submitForm.get_time)) {
- submitForm.get_time = submitForm.get_time.join(' - ');
- const res =
- ? await api.add(submitForm)
- : await api.edit(props.modal.params.id, submitForm);
- if (res.error == 0) {
- uuID: props.modal.params.uuID,
- .el-range-editor.el-input__inner {
- // width: 360px;
- .sa-template-wrap {
- .key {
- width: 400px;
- .oper {
- width: 60px;
- .item {
- border-bottom: 1px solid var(--sa-space);
- & > .el-form-item {
- margin-bottom: 12px;
- .goods {
- .goods-title {
- .goods-price {
- color: var(--el-color-danger);
- .goods-stock {
- &.sa-template-wrap-activity {
- width: 464px;
- //Switch开启颜色
- .get-time {
- max-width: 500px;
@@ -1,506 +0,0 @@
- <el-container class="coupon-view panel-block">
- <el-row class="sa-m-t-20" :gutter="20">
- <el-col v-for="(value, key) in dashboard" :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
- <div class="card">
- <div class="sa-flex sa-row-between sa-m-b-4">
- <div class="num">{{ value.num }}</div>
- <div class="sa-flex sa-row-between">
- <div class="name">{{ value.name }}</div>
- <el-popover popper-class="sa-popper" trigger="hover">
- {{ value.tip }}
- <el-icon class="tip">
- <warning />
- <span class="left">{{ title }}</span>
- <el-button v-auth="'shop.admin.coupon.add'" type="primary" icon="Plus" @click="addRow"
- >添加</el-button
- v-auth="'shop.admin.coupon.recyclebin'"
- <el-table height="100%" class="sa-table" :data="table.data" stripe>
- <el-table-column prop="id" label="ID" min-width="88" sortable></el-table-column>
- <el-table-column label="优惠券名称" min-width="172">
- <el-table-column label="类型" min-width="74">
- <el-table-column label="可用范围" min-width="116">
- <div class="sa-table-line-1">{{ scope.row.use_scope_text }}</div>
- <el-table-column label="优惠内容" min-width="214">
- <div class="sa-table-line-1">{{ scope.row.amount_text }}</div>
- <el-table-column label="领取状态" min-width="80" align="center">
- placement="bottom"
- title="优惠券有效期"
- trigger="hover"
- popper-class="sa-popper"
- class="sa-table-line-1 get-time-text"
- scope.row.get_time_text == '发放中'
- ? 'success'
- : scope.row.get_time_text == '已结束'
- ? 'sa-delete'
- : 'info'
- {{ scope.row.get_time_text }}
- <div v-if="scope.row.use_time_type == 'days'">
- 领取{{ scope.row.start_days }}天后生效,有效期{{ scope.row.days }}天
- <div v-if="scope.row.use_time_type == 'range'">
- <div>开始时间:{{ scope.row.use_start_time }}</div>
- <div>结束时间:{{ scope.row.use_end_time }}</div>
- <el-table-column label="已领取" min-width="96">
- <div class="sa-table-line-1">{{ scope.row.get_num }}</div>
- <el-table-column label="已使用" min-width="96">
- <div class="sa-table-line-1">{{ scope.row.use_num }}</div>
- <el-table-column label="剩余" min-width="96">
- <div class="sa-table-line-1">{{ scope.row.stock }}</div>
- <el-table-column label="状态" min-width="130">
- <el-dropdown trigger="click" @command="handleCommand">
- <el-button v-auth="'shop.admin.coupon.edit'" class="status-btn">
- {{ scope.row.status_text }}
- <el-icon class="el-icon--right">
- <template #dropdown>
- <el-dropdown-menu>
- <el-dropdown-item
- :command="{
- id: scope.row.id,
- type: 'normal',
- <!-- TODO-jj:弹框提醒 -->
- 公开发放
- <!-- <template #default>
- <el-popconfirm width="fit-content"
- @confirm="handConfirm(scope.row.id)"
- <el-button class="sa-button" text> 公开发放 </el-button>
- </template> -->
- </el-dropdown-item>
- type: 'hidden',
- >后台发放</el-dropdown-item
- type: 'disabled',
- >禁止使用</el-dropdown-item
- </el-dropdown-menu>
- </el-dropdown>
- <el-table-column label="操作" min-width="260" fixed="right">
- v-auth="'shop.admin.coupon.send'"
- @click="onSend(scope.row.id)"
- >手动发放</el-button
- v-auth="'shop.admin.user.coupon.couponlist'"
- @click="onCoupon(scope.row.id)"
- >领取记录</el-button
- v-auth="'shop.admin.coupon.detail'"
- @click="editRow(scope.row.id)"
- <el-button v-auth="'shop.admin.coupon.delete'" class="is-link" type="danger">
- import CouponEdit from './edit.vue';
- import ApiRecyclebin from './recyclebin.vue';
- import { useRouter, useRoute } from 'vue-router';
- import UserSelect from '@/app/user/admin/select.vue';
- const router = useRouter();
- label: '搜索内容',
- type: {
- type: 'tselect',
- label: '类型',
- field: 'type',
- data: [
- label: '满减券',
- value: 'reduce',
- label: '折扣券',
- value: 'discount',
- use_scope: {
- label: '可用范围',
- field: 'use_scope',
- label: '全场通用',
- value: 'all_use',
- label: '指定商品可用',
- value: 'goods',
- label: '指定商品不可用',
- value: 'disabled_goods',
- label: '指定分类可用',
- value: 'category',
- keyword: '',
- type: '',
- use_scope: '',
- const dashboard = reactive({
- total_num: {
- name: '总发券量/张',
- num: '',
- tip: '用户领取的优惠券的总张数,包含已经被后台删除的优惠券'
- expire_num: {
- name: '已过期/张',
- tip: '用户已领取的并且已经超过可使用日期的未使用优惠券'
- use_num: {
- name: '已使用/张',
- tip: '用户已领取并且已使用的优惠券',
- use_percent: {
- name: '使用率',
- tip: '用户已使用优惠和总发券量的比例'
- // 分页
- const title = ref('优惠券');
- const statusId = ref(0);
- function handConfirm(e) {
- statusId.value = e;
- //获取状态
- async function handleCommand(e) {
- await api.edit(e.id, {
- status: e.type,
- let search = composeFilter(tempSearch);
- const { data } = await api.list({
- table.data = data.coupons.data;
- pageData.page = data.coupons.current_page;
- pageData.list_rows = data.coupons.per_page;
- pageData.total = data.coupons.total;
- for (var key in dashboard) {
- dashboard[key].num = data[key]
- CouponEdit,
- title: `添加${title.value}`,
- atype: 'full_coupon',
- title: `编辑${title.value}`,
- await api.delete(id);
- ApiRecyclebin,
- close: () => {
- function onCoupon(id) {
- router.push({
- path: '/shop/admin/user/couponList',
- query: {
- couponid: id,
- function onSend(id) {
- UserSelect,
- title: `选择用户`,
- .coupon-view {
- .card {
- height: 80px;
- border: 1px solid var(--sa-space);
- box-shadow: 0px 2px 6px rgba(140, 140, 140, 0.12);
- padding: 16px;
- margin-bottom: 16px;
- .card .num {
- line-height: 26px;
- .card .oper {
- line-height: 14px;
- color: #FAAD14;
- .card .name {
- .card .tip {
- .sa-title {
- padding-top: 0;
- .get-time-text {
- .status-btn {
- background-color: var(--sa-space);
- .success {
- .info {
- <div class="sa-table-line-1">{{ scope.row.name || '-' }}</div>
- min-width="160"
- v-auth="'shop.admin.coupon.restore'"
- <el-button v-auth="'shop.admin.coupon.destroy'" class="is-link" type="danger">
- 销毁
- const { data } = await api.recyclebin({
- auth: 'shop.admin.coupon.restore',
- auth: 'shop.admin.coupon.destroy',
- async function batchHandle(type, id) {
- if (id == 'all') {
- await api.destroy(id);
- await api.restore(id);
@@ -1,197 +0,0 @@
- <el-container class="panel-block coupon-select">
- <el-main class="coupon-main">
- <el-table-column v-if="modal?.params?.multiple" type="selection" />
- <el-table-column label="优惠券名称" min-width="128">
- {{ scope.row.name }}
- {{ scope.row.type_text }}
- <el-table-column label="可用范围" min-width="88">
- {{ scope.row.use_scope_text }}
- <el-table-column label="优惠内容" min-width="154">
- {{ scope.row.amount_text }}
- <el-table-column label="领取状态" min-width="80">
- <el-table-column label="剩余" min-width="88">
- {{ scope.row.stock }}
- <el-table-column v-if="!modal?.params?.multiple" label="操作" min-width="88">
- <el-button class="is-link" type="primary" @click="modalCallBack(scope.row)"
- <el-footer class="sa-footer--submit sa-flex sa-row-between sa-flex-wrap">
- <el-pagination
- v-if="pageData"
- @size-change="changeSize"
- @current-change="changeCurrent"
- v-model:currentPage="pageData.page"
- :page-sizes="[10, 50, 100]"
- :page-size="pageData.list_rows"
- layout="total, sizes, prev, pager, next, jumper"
- :total="pageData.total"
- :pager-count="5"
- ></el-pagination>
- <el-button v-if="modal?.params?.multiple" type="primary" @click="confirm">确定</el-button>
- name: 'CouponSelect',
- default: {
- params: {
- multiple: false,
- let search = {};
- if (props.modal?.params?.status) {
- search = JSON.stringify({ status: [props.modal.params.status] });
- const { data } = await api.select({
- search: search,
- const pageData = reactive({
- page: 1,
- list_rows: 10,
- total: 0,
- // 选择分页数量
- function changeSize(list_rows) {
- pageData.list_rows = list_rows;
- // 选择分页
- function changeCurrent(page) {
- pageData.page = page;
- function modalCallBack(data) {
- emit('modalCallBack', { event: 'confirm', data });
@@ -30,19 +30,6 @@ const api = {
method: 'GET',
- express: {
- list: () =>
- url: '/shop/admin/data/express/list',
- track: (code, no) =>
- url: '/shop/admin/data/express/track',
- params: { code, no },
export { route, api };
@@ -57,7 +57,7 @@
</el-col>
</el-row>
<!-- 数据表格 -->
<div class="sa-table-wrap panel-block panel-block--bottom" v-loading="loading">
<el-table
@@ -76,9 +76,7 @@
<el-table-column prop="date" label="日期" min-width="120" sortable="custom">
<el-table-column label="销售额" min-width="120">
- ৳{{ scope.row.sales_amount || 0 }}
+ <template #default="scope"> ৳{{ scope.row.sales_amount || 0 }} </template>
<el-table-column label="订单数" min-width="100">
@@ -96,14 +94,10 @@
<el-table-column label="转化率" min-width="100">
- {{ scope.row.conversion_rate || 0 }}%
+ <template #default="scope"> {{ scope.row.conversion_rate || 0 }}% </template>
<el-table-column label="客单价" min-width="120">
- ৳{{ scope.row.avg_order_value || 0 }}
+ <template #default="scope"> ৳{{ scope.row.avg_order_value || 0 }} </template>
</el-table>
@@ -124,7 +118,7 @@
- import { api } from './report.service';
+ import { api } from '../data.service';
import { ElMessage } from 'element-plus';
const { pageData } = usePagination();
@@ -142,7 +136,7 @@
const defaultSearchValues = reactive({
date_range: [],
});
// 统计数据
const statsData = reactive({
totalSales: 0,
@@ -150,7 +144,7 @@
totalUsers: 0,
totalGoods: 0,
// 列表
const table = reactive({
data: [],
@@ -159,20 +153,20 @@
selected: [],
const loading = ref(true);
// 获取统计数据
async function getStatsData() {
- const { error, data } = await api.stats();
+ const { error, data } = await api.report.getStats();
if (error === 0) {
Object.assign(statsData, data);
}
// 获取
async function getData(page, searchParams = {}) {
if (page) pageData.page = page;
loading.value = true;
- const { error, data } = await api.list({
+ const { error, data } = await api.report.list({
page: pageData.page,
list_rows: pageData.list_rows,
order: table.order,
@@ -188,24 +182,24 @@
loading.value = false;
// table 字段排序
function fieldFilter({ prop, order }) {
table.order = order == 'ascending' ? 'asc' : 'desc';
table.sort = prop;
getData();
//table批量选择
function changeSelection(row) {
table.selected = row;
// 导出数据
async function exportData() {
ElMessage.success('导出功能开发中...');
// 分页/批量操作
const batchHandleTools = [
{
@@ -215,7 +209,7 @@
class: 'primary',
];
async function batchHandle(type) {
let ids = [];
table.selected.forEach((row) => {
@@ -247,7 +241,7 @@
.stats-value {
font-size: 24px;
font-weight: bold;
- color: #409EFF;
+ color: #409eff;
margin-bottom: 8px;
.stats-label {
@@ -1,27 +0,0 @@
- path: 'report',
- name: 'shop.admin.data.report',
- component: () => import('@/app/shop/admin/data/report/index.vue'),
- title: '数据报表',
- ...CRUD('shop/admin/data_report'),
- select: (params) => SELECT('shop/admin/data_report', params),
- stats: () => ({
- error: 0,
- msg: '获取成功',
- totalSales: 125680.50,
- totalOrders: 1256,
- totalUsers: 3456,
- totalGoods: 234,
@@ -1,168 +0,0 @@
- <el-container class="page-preview">
- <el-row :gutter="20">
- <el-col class="sa-col-12" :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
- props.modal.params.data.type == 'diypage' ||
- state.templateData.platform.includes('H5') ||
- state.templateData.platform.includes('WechatOfficialAccount')
- class="left sa-flex-col sa-col-center"
- <div class="preview-title">此为预览效果,实际效果请扫码查看</div>
- <div class="web-preview">
- <div v-if="isShowIframe" class="web-preview-msg">
- <span v-html="isShowIframe"></span>
- <iframe v-else id="preview" :src="url.H5" frameborder="1" height="600px"></iframe>
- <div class="name">{{ state.templateData.name }}</div>
- <template
- props.modal.params.data.type == 'diypage' || state.templateData.platform.length > 0
- <div class="platform sa-m-b-20">
- <sa-icon
- v-for="pl in state.templateData.platform"
- :key="pl"
- :icon="'sa-shop-decorate-' + pl"
- :style="{
- color: platformList.filter((pf) => {
- return pf.type == pl;
- })[0].color,
- size="20"
- class="h5"
- <qrcode-vue :value="url.H5" :size="132" level="H" />
- <div class="tip sa-m-t-12">微信扫描二维码即可预览</div>
- <div class="copyright">星品科技Shopro版权所有 Copyright 2020-2022</div>
- import { computed, onMounted, reactive } from 'vue';
- import { api as configApi } from '@/app/shop/admin/config/config.service';
- import { api } from '../decorate.service';
- import { platformList } from '../page/data';
- import QrcodeVue from 'qrcode.vue';
- templateData: props.modal.params.data,
- domain: '',
- async function getConfig() {
- const { error, data } = await configApi.basic();
- error === 0 && (state.domain = data.domain);
- const url = computed(() => {
- return {
- H5: `${props.modal.params.data.h5Url}`,
- WechatMiniProgram: `${props.modal.params.data.wxacode}`,
- const isShowIframe = computed(() => {
- if (window.location.protocol == 'https:' && url.value.H5.split('://')[0] == 'http') {
- return '您的商城前端域名ssl未开启,<br/>请扫码预览';
- if (!state.domain) {
- return '请在商城配置设置您的前端域名';
- getConfig();
-<style lang="scss">
- .sa-dialog.preview-dialog {
- max-height: 76vh;
- margin: 12vh auto;
- max-height: 100vh;
- margin: 0;
- .page-preview {
- --el-main-padding: 20px 20px 0 20px;
- .left {
- margin-bottom: 20px;
- .preview-title {
- .web-preview {
- height: 594px;
- background: url('/static/images/shop/decorate/preview_bg.png');
- padding: 18px;
- #preview {
- margin: 0 auto;
- border-radius: 26px;
- .name {
- .copyright {
- .h5,
- .wechat {
- flex-direction: column;
- .tip {
- margin-bottom: 24px;
@@ -1,1353 +0,0 @@
-import { checkUrl } from '@/sheep/utils/checkUrlSuffix';
-import { api as goodsApi } from '@/app/shop/admin/goods/goods.service';
-import { api as couponApi } from '@/app/shop/admin/coupon/coupon.service';
-import { api as dataApi } from '@/app/shop/admin/data/data.service';
-import { api as activityApi } from '@/app/shop/admin/activity/activity.service';
-import { api as appApi } from '@/app/shop/admin/app/app.service';
-// 主题
-export const themeColor = {
- orange: {
- color1: '#FF6000',
- color2: '#FE832A',
- golden: {
- color1: '#E9B461',
- color2: '#EECC89',
- yellow: {
- color1: '#FFC300',
- color2: '#FDDF47',
- black: {
- color1: '#484848',
- color2: '#6D6D6D',
- green: {
- color1: '#2AAE67',
- color2: '#3ACD72',
- purple: {
- color1: '#652ABF',
- color2: '#A36FFF',
-// 页面列表
-export const pageTypeList = [
- type: 'basic',
- label: '基础配置',
- type: 'home',
- label: '首页',
- type: 'user',
- label: '个人页',
-];
-// 系统列表
-export const systemList = [
- type: 'android',
- label: 'Android',
- color: '#6F74E9',
- type: 'ios',
- label: 'IOS',
- color: '#333333',
-// 平台列表
-export const platformList = [
- type: 'H5',
- label: 'H5',
- color: '#FC800E',
- type: 'App',
- label: 'APP',
- color: '#806AF6',
-// 基础列表
-export const basicList = [
- name: '应用设置',
- // {
- // name: '启动页',
- // type: 'splashScreen',
- // { name: '引导页', type: 'guidePage' },
- name: '底部导航',
- type: 'tabbar',
- name: '悬浮按钮',
- type: 'floatMenu',
- name: '弹窗广告',
- type: 'popupImage',
- name: '主题色',
- type: 'theme',
- name: '淘宝橙',
- type: 'orange',
- name: '香槟金',
- type: 'golden',
- name: '美团黄',
- type: 'yellow',
- name: '低奢黑',
- type: 'black',
- name: '微信绿',
- type: 'green',
- name: '尊贵紫',
- type: 'purple',
-// 组件列表
-export const compList = [
- name: '会员组件',
- type: '0',
- show: ['user', 'diypage'],
- name: '会员卡片',
- type: 'userCard',
- name: '订单卡片',
- type: 'orderCard',
- name: '资产卡片',
- type: 'walletCard',
- name: '卡券卡片',
- type: 'couponCard',
- name: '基础组件',
- type: '1',
- name: '搜索框',
- type: 'searchBlock',
- name: '公告栏',
- type: 'noticeBlock',
- name: '菜单导航',
- type: 'menuButton',
- name: '列表导航',
- type: 'menuList',
- name: '宫格导航',
- type: 'menuGrid',
- name: '商品组件',
- type: '2',
- name: '商品卡片',
- type: 'goodsCard',
- name: '商品栏',
- type: 'goodsShelves',
- name: '图文组件',
- type: '3',
- name: '图片展示',
- type: 'imageBlock',
- name: '图片轮播',
- type: 'imageBanner',
- name: '标题栏',
- type: 'titleBlock',
- name: '广告魔方',
- type: 'imageCube',
- name: '视频播放',
- type: 'videoPlayer',
- name: '辅助线',
- type: 'lineBlock',
- name: '富文本',
- type: 'richtext',
- name: '热区',
- type: 'hotzone',
- name: '营销组件',
- type: '4',
- name: '拼团',
- type: 'groupon',
- name: '秒杀',
- type: 'seckill',
- name: '积分商城',
- type: 'scoreGoods',
- { name: '小程序直播', type: 'mplive' },
- name: '优惠券',
- type: 'coupon',
- // name: '关注公众号',
- // type: 'subscribeWechatOfficialAccount',
-// 页面默认数据
-export const initTemplateData = {
- basic: {
- // splashScreen: {
- // status: false, // false|true
- // src: '',
- // countdown: 5,
- // url: '',
- // guidePage: {
- // list: [],
- tabbar: {
- mode: 1, // 1 2
- layout: 1, // 1=文字+图片 2=文字 3=图片
- inactiveColor: '#EEEEEE',
- activeColor: '#000000',
- list: [
- // inactiveIcon: '',
- // activeIcon: '',
- // text: '',
- background: {
- type: 'color', // color=纯色 image=背景图
- bgImage: '',
- bgColor: '#FFFFFF',
- floatMenu: {
- show: 0, // 0|1
- mode: 1, // 1|2
- isText: 0, // 0|2
- src: '',
- url: '',
- title: {
- text: '',
- color: '',
- popupImage: {
- // show:1
- theme: 'orange',
- home: {
- style: {
- color: '#F6F6F6',
- navbar: {
- mode: 'normal', // normal inner
- alwaysShow: 0, // 0 1
- type: 'color',
- list: {
- mp: [],
- app: [],
- marginLeft: 0,
- marginRight: 0,
- marginTop: 0,
- marginBottom: 10,
- borderRadiusTop: 0,
- borderRadiusBottom: 0,
- diypage: {
-// 组件数据
-export function cloneComponent(type, theme = 'orange') {
- let comp = {
- userCard: {
- padding: 0,
- orderCard: {
- walletCard: {
- couponCard: {
- searchBlock: {
- placeholder: '',
- borderRadius: 0,
- keywords: [
- // color: '#8C8C8C',
- noticeBlock: {
- mode: 1,
- src: checkUrl('/static/img/shop/decorate/notice-1.png'),
- color: '#111111',
- menuButton: {
- layout: 1, // 1=图片+文字 2=图片
- col: 3, // 列数 3|4|5
- row: 1, // 1 2 3行数 超出滑动
- // title: {
- // color: '#000'
- // badge: {
- // show: 0, // 0|1
- // color: '#FFFFFF',
- // bgColor: '#FF6000',
- menuList: {
- // color: '#333'
- // tip: {
- // color: '#bbb'
- menuGrid: {
- col: 3, // 列数 3|4
- // border: 0, // 边框 0|1
- color: '#333',
- tip: {
- color: '#bbb',
- badge: {
- color: '#FFFFFF',
- bgColor: '#FF6000',
- goodsCard: {
- goodsFields: {
- show: 1, // 0|1
- color: '#000',
- subtitle: {
- color: '#999',
- price: {
- color: '#ff3000',
- original_price: {
- color: '#c4c4c4',
- sales: {
- stock: {
- buyNowStyle: {
- text: '立即购买',
- color1: themeColor[theme].color1,
- color2: themeColor[theme].color2,
- tagStyle: {
- show: 0,
- goodsIds: [],
- goodsList: [],
- space: 8,
- bgColor: '',
- marginLeft: 8,
- marginRight: 8,
- goodsShelves: {
- space: 0,
- imageBlock: {
- // height: 300,
- imageBanner: {
- indicator: 1, // 1 2
- autoplay: false,
- interval: 3000,
- // title: '',
- // type: 'image',
- // poster: '',
- // url: ''
- titleBlock: {
- src: checkUrl('/static/img/shop/decorate/title-1.png'),
- location: 'left', // left=居左 center=居中
- skew: 0,
- text: '标题栏',
- textFontSize: 14,
- other: [], // bold=加粗 italic=倾斜
- text: '副标题',
- color: '#8c8c8c',
- textFontSize: 12,
- more: {
- show: 0, // 0=不显示 1=显示
- marginBottom: 0,
- height: 40,
- imageCube: {
- // width: 0,
- // height: 0,
- // top: 0,
- // left: 0,
- videoPlayer: {
- videoUrl: '', // 视频地址
- src: '', // 视频封面
- height: 300,
- lineBlock: {
- mode: 'solid', // solid dotted dashed
- lineColor: '',
- richtext: {
- id: '',
- title: '',
- hotzone: {
- list: [],
- // marginLeft: 0,
- // marginRight: 0,
- // marginTop: 0,
- // marginBottom: 10,
- // padding: 0,
- groupon: {
- activityId: '',
- activityList: [],
- show: 0, // 0,1
- color: '#FF0000',
- color: '#C4C4C4',
- text: '立即拼团',
- seckill: {
- text: '去抢购',
- scoreGoods: {
- score_price: {
- color: '#FF3000',
- text: '去兑换',
- type: 'mplive',
- color: '#FDFDFD',
- anchor_name: {
- mpliveIds: [],
- mpliveList: [],
- coupon: {
- couponIds: [],
- couponList: [],
- fill: {
- button: {
- // subscribeWechatOfficialAccount: {
- // data: {
- // textColor: ''
- // subtitle: {
- // style: {
- // background: {
- // type: 'color',
- // bgImage: '',
- // bgColor: '#FFFFFF',
- // borderRadiusTop: 0,
- // borderRadiusBottom: 0,
- return comp[type];
-// 组件名称
-export const compNameObj = {
- // splashScreen: '启动页',
- // guidePage: '引导页',
- tabbar: '底部导航',
- floatMenu: '悬浮按钮',
- popupImage: '弹窗广告',
- page: '页面设置',
- userCard: '会员卡片',
- orderCard: '订单卡片',
- walletCard: '资产卡片',
- couponCard: '卡券卡片',
- searchBlock: '搜索框',
- noticeBlock: '公告栏',
- menuButton: '菜单导航',
- menuList: '列表导航',
- menuGrid: '宫格导航',
- goodsCard: '商品卡片',
- goodsShelves: '商品栏',
- imageBlock: '图片展示',
- imageBanner: '图片轮播',
- titleBlock: '标题栏',
- imageCube: '广告魔方',
- videoPlayer: '视频播放',
- lineBlock: '辅助线',
- richtext: '富文本',
- hotzone: '热区',
- groupon: '拼团',
- seckill: '秒杀',
- scoreGoods: '积分商城',
- coupon: '优惠券',
-// 初始化暂存数据
-export function handleTempData(data) {
- data.data.forEach(async (t) => {
- if (['goodsCard', 'goodsShelves'].includes(t.type)) {
- const { error, data } = await goodsApi.goods.select(
- search: JSON.stringify({ id: [t.data.goodsIds.join(','), 'in'] }),
- t.data.goodsList = error === 0 ? data : [];
- } else if (t.type == 'coupon') {
- const { error, data } = await couponApi.select(
- search: JSON.stringify({ id: [t.data.couponIds.join(','), 'in'], status: ['normal'] }),
- t.data.couponList = error === 0 ? data : [];
- } else if (t.type == 'richtext') {
- const { error, data } = await dataApi.richtext.select(
- search: JSON.stringify({ id: [t.data.id, 'in'] }),
- 'find',
- t.data.richtext = error === 0 ? data : [];
- } else if (t.type == 'groupon' || t.type == 'seckill') {
- if (t.data.activityId) {
- const detail = await activityApi.activity.detail(t.data.activityId);
- if (detail.error === 0) {
- t.data.activityList = [detail.data];
- const { error, data } = await goodsApi.goods.activitySelect({
- activity_id: t.data.activityId,
- need_buyers: t.type == 'groupon' ? 1 : 0,
- t.data.activityList = [];
- t.data.goodsList = [];
- } else if (t.type == 'scoreGoods') {
- const { error, data } = await appApi.scoreShop.select(
- } else if (t.type == 'mplive') {
- const { error, data } = await appApi.mplive.room.select({
- search: JSON.stringify({ roomid: [t.data.mpliveIds.join(','), 'in'] }),
- t.data.mpliveList = error === 0 ? data : [];
- return data;
-// 提交删除多余数据
-export function handleSubmitData(data) {
- data.data.forEach((t) => {
- t.data.goodsIds = [];
- t.data.goodsList.forEach((g) => {
- t.data.goodsIds.push(g.id);
- delete t.data.goodsList;
- t.data.couponIds = [];
- t.data.couponList?.forEach((c) => {
- t.data.couponIds.push(c.id);
- delete t.data.couponList;
- delete t.data.richtext;
- t.data.activityId = [];
- t.data.activityList.forEach((c) => {
- t.data.activityId.push(c.id);
- t.data.activityId = t.data.activityId.join(',');
- delete t.data.activityList;
- t.data.goodsList.forEach((c) => {
- t.data.goodsIds.push(c.id);
- t.data.mpliveIds = [];
- t.data.mpliveList.forEach((c) => {
- t.data.mpliveIds.push(c.roomid);
- delete t.data.mpliveList;
@@ -1,173 +0,0 @@
- state.templateData.platform.includes('H5')
- <div class="tip sa-m-t-12">扫描二维码即可预览</div>
- import { platformList } from './data';
- if (props.modal.params.data.type == 'diypage') {
- H5: `${state.domain}pages/index/page?id=${state.templateData.id}`,
- WechatMiniProgram: api.getWxacode(`/pages/index/page?id=${state.templateData.id}`),
- H5: `${state.domain}pages/index/index?templateId=${state.templateData.id}`,
- WechatMiniProgram: api.getWxacode(`/pages/index/index?templateId=${state.templateData.id}`),
- .wechatminiprogram {
- width: 132px;
- height: 132px;
@@ -1,89 +0,0 @@
- <el-form-item label="模板名称" prop="name">
- <el-input v-model="form.model.name" placeholder="请输入模板名称"></el-input>
- <el-form-item label="备注">
- <el-input type="textarea" v-model="form.model.memo" placeholder="请输入备注"></el-input>
- <el-form-item v-if="modal?.params.templateType == 'template'" label="发布平台">
- <el-checkbox-group v-model="form.model.platform">
- <el-checkbox label="H5">H5</el-checkbox>
- <el-checkbox label="App">APP</el-checkbox>
- </el-checkbox-group>
- v-auth="'shop.admin.decorate.template.add'"
- v-auth="'shop.admin.decorate.template.edit'"
- type: props.modal.params.templateType,
- memo: '',
- platform: [],
- const { error, data } = await api.template.detail(id);
- error === 0 && (form.model = data);
- ? await api.template.add(submitForm)
- : await api.template.edit(props.modal.params.id, submitForm);
- getDetail(props.modal.params.id);
- path: 'commission',
- name: 'shop.admin.finance.commission',
- component: () => import('@/app/shop/admin/finance/commission/index.vue'),
- title: '佣金',
- ...CRUD('shop/admin/commission'),
- select: (params) => SELECT('shop/admin/commission', params),
- settle: (id) => ({
- msg: '结算成功',
- data: null,
- batchSettle: (ids) => ({
- msg: '批量结算成功',
@@ -12,14 +12,14 @@
<el-form-item label="佣金金额" prop="amount">
- <el-input-number
+ <el-input-number
+ v-model="form.model.amount"
+ :min="0"
:precision="2"
placeholder="佣金金额"
:disabled="isView"
- <span style="margin-left: 10px;">৳</span>
+ <span style="margin-left: 10px">৳</span>
<el-form-item label="佣金类型" prop="type">
<el-select v-model="form.model.type" placeholder="请选择佣金类型" :disabled="isView">
@@ -29,18 +29,22 @@
<el-form-item label="来源订单" prop="source_order_no">
- <el-input v-model="form.model.source_order_no" placeholder="请填写来源订单号" :disabled="isView"></el-input>
+ v-model="form.model.source_order_no"
+ placeholder="请填写来源订单号"
+ :disabled="isView"
+ ></el-input>
<el-form-item label="佣金比例" prop="commission_rate">
- v-model="form.model.commission_rate"
+ v-model="form.model.commission_rate"
:max="100"
placeholder="佣金比例"
- <span style="margin-left: 10px;">%</span>
+ <span style="margin-left: 10px">%</span>
<el-form-item label="状态" prop="status">
<el-select v-model="form.model.status" placeholder="请选择状态" :disabled="isView">
@@ -56,9 +60,9 @@
<span>{{ form.model.settle_time || '-' }}</span>
<el-form-item label="备注" prop="remark">
- v-model="form.model.remark"
+ v-model="form.model.remark"
placeholder="请填写备注"
@@ -75,16 +79,16 @@
import { onMounted, reactive, ref, unref, computed } from 'vue';
- import { api } from './commission.service';
+ import { api } from '../finance.service';
type: Object,
const isView = computed(() => props.modal.params.type === 'view');
// 添加 编辑 form
let formRef = ref(null);
const form = reactive({
@@ -113,7 +117,7 @@
// 获取详情
async function getDetail(id) {
- const { error, data } = await api.detail(id);
+ const { error, data } = await api.commission.detail(id);
error === 0 && (form.model = data);
@@ -124,8 +128,8 @@
let submitForm = cloneDeep(form.model);
const { error } =
props.modal.params.type == 'add'
+ ? await api.commission.add(submitForm)
+ : await api.commission.edit(props.modal.params.id, submitForm);
if (error == 0) {
emit('modalCallBack', { event: 'confirm' });
@@ -45,9 +45,7 @@
<el-table-column label="佣金金额" min-width="120">
- ৳{{ scope.row.amount || 0 }}
+ <template #default="scope"> ৳{{ scope.row.amount || 0 }} </template>
<el-table-column label="佣金类型" min-width="120">
@@ -64,9 +62,7 @@
<el-table-column label="佣金比例" min-width="100">
- {{ scope.row.commission_rate || 0 }}%
+ <template #default="scope"> {{ scope.row.commission_rate || 0 }}% </template>
<el-table-column label="状态" min-width="100">
@@ -88,10 +84,10 @@
<el-table-column fixed="right" label="操作" min-width="120">
<el-button class="is-link" type="primary" @click="editRow(scope.row)">查看</el-button>
+ <el-button
v-if="scope.row.status === 'pending'"
- type="success"
+ class="is-link"
+ type="success"
@click="settleCommission(scope.row)"
>
结算
@@ -117,7 +113,7 @@
import { ElMessageBox, ElMessage } from 'element-plus';
@@ -157,7 +153,7 @@
// 获取状态类型
function getStatusType(status) {
const statusMap = {
@@ -167,12 +163,12 @@
return statusMap[status] || 'info';
+ const { error, data } = await api.commission.list({
@@ -233,7 +229,7 @@
);
// 结算佣金
async function settleCommission(row) {
ElMessageBox.confirm('确认结算该笔佣金?', '提示', {
@@ -241,14 +237,14 @@
cancelButtonText: '取消',
type: 'warning',
}).then(async () => {
- const { error } = await api.settle(row.id);
+ const { error } = await api.commission.settle(row.id);
ElMessage.success('结算成功');
@@ -261,7 +257,7 @@
- const { error } = await api.batchSettle(ids.join(','));
+ const { error } = await api.commission.batchSettle(ids.join(','));
ElMessage.success('批量结算成功');
@@ -0,0 +1,99 @@
+ path: 'finance',
+ name: 'shop.admin.finance',
+ title: '财务',
+ path: 'commission',
+ name: 'shop.admin.finance.commission',
+ component: () => import('@/app/shop/admin/finance/commission/index.vue'),
+ title: '佣金管理',
+ path: 'recharge',
+ name: 'shop.admin.finance.recharge',
+ component: () => import('@/app/shop/admin/finance/recharge/index.vue'),
+ title: '充值管理',
+ path: 'withdraw',
+ name: 'shop.admin.finance.withdraw',
+ component: () => import('@/app/shop/admin/finance/withdraw/index.vue'),
+ title: '提现管理',
+ // 佣金相关 API
+ commission: {
+ ...CRUD('shop/admin/finance/commission'),
+ select: (params) => SELECT('shop/admin/finance/commission', params),
+ settle: (id) => ({
+ error: 0,
+ msg: '结算成功',
+ data: null,
+ batchSettle: (ids) => ({
+ msg: '批量结算成功',
+ // 充值相关 API
+ recharge: {
+ ...CRUD('shop/admin/finance/recharge'),
+ select: (params) => SELECT('shop/admin/finance/recharge', params),
+ confirm: (id) => ({
+ msg: '确认成功',
+ batchConfirm: (ids) => ({
+ msg: '批量确认成功',
+ // 提现相关 API
+ withdraw: {
+ ...CRUD('shop/admin/finance/withdraw'),
+ select: (params) => SELECT('shop/admin/finance/withdraw', params),
+ approve: (id) => ({
+ msg: '通过成功',
+ reject: (id, data) => ({
+ msg: '拒绝成功',
+ batchApprove: (ids) => ({
+ msg: '批量通过成功',
+ batchReject: (ids, data) => ({
+ msg: '批量拒绝成功',
@@ -3,20 +3,28 @@
<el-main>
<el-form :model="form.model" :rules="form.rules" ref="formRef" label-width="100px">
<el-form-item label="用户ID" prop="user_id">
- <el-input v-model="form.model.user_id" placeholder="请填写用户ID" :disabled="isView"></el-input>
+ v-model="form.model.user_id"
+ placeholder="请填写用户ID"
<el-form-item label="充值金额" prop="amount">
placeholder="充值金额"
<el-form-item label="支付方式" prop="payment_method">
- <el-select v-model="form.model.payment_method" placeholder="请选择支付方式" :disabled="isView">
+ <el-select
+ v-model="form.model.payment_method"
+ placeholder="请选择支付方式"
+ >
<el-option label="支付宝" value="alipay"></el-option>
<el-option label="微信支付" value="wechat"></el-option>
<el-option label="银行卡" value="bank"></el-option>
@@ -24,7 +32,11 @@
<el-form-item label="订单号" prop="order_no">
- <el-input v-model="form.model.order_no" placeholder="请填写订单号" :disabled="isView"></el-input>
+ v-model="form.model.order_no"
+ placeholder="请填写订单号"
@@ -34,9 +46,9 @@
@@ -53,16 +65,16 @@
- import { api } from './recharge.service';
@@ -86,7 +98,7 @@
+ const { error, data } = await api.recharge.detail(id);
@@ -97,8 +109,8 @@
+ ? await api.recharge.add(submitForm)
+ : await api.recharge.edit(props.modal.params.id, submitForm);
<el-table-column label="充值金额" min-width="120">
<el-table-column label="支付方式" min-width="120">
@@ -83,10 +81,10 @@
@click="confirmRecharge(scope.row)"
确认
@@ -112,7 +110,7 @@
@@ -152,7 +150,7 @@
@@ -162,12 +160,12 @@
+ const { error, data } = await api.recharge.list({
@@ -228,7 +226,7 @@
// 确认充值
async function confirmRecharge(row) {
ElMessageBox.confirm('确认该笔充值记录?', '提示', {
@@ -236,14 +234,14 @@
- const { error } = await api.confirm(row.id);
+ const { error } = await api.recharge.confirm(row.id);
ElMessage.success('确认成功');
@@ -256,7 +254,7 @@
- const { error } = await api.batchConfirm(ids.join(','));
+ const { error } = await api.recharge.batchConfirm(ids.join(','));
ElMessage.success('批量确认成功');
- path: 'recharge',
- name: 'shop.admin.finance.recharge',
- component: () => import('@/app/shop/admin/finance/recharge/index.vue'),
- title: '充值',
- ...CRUD('shop/admin/recharge'),
- select: (params) => SELECT('shop/admin/recharge', params),
- confirm: (id) => ({
- msg: '确认成功',
- batchConfirm: (ids) => ({
- msg: '批量确认成功',
@@ -60,13 +60,13 @@
- import { api } from './withdraw.service';
// 表单数据
model: {
@@ -92,9 +92,9 @@
remark: '',
const loading = ref(false);
@@ -105,21 +105,21 @@
+ const { error, data } = await api.withdraw.detail(id);
async function init() {
if (props.modal.params.id) {
await getDetail(props.modal.params.id);
onMounted(() => {
init();
@@ -44,19 +44,13 @@
<el-table-column label="提款金额" min-width="120">
<el-table-column label="手续费" min-width="100">
- ৳{{ scope.row.fee || 0 }}
+ <template #default="scope"> ৳{{ scope.row.fee || 0 }} </template>
<el-table-column label="实际到账" min-width="120">
- ৳{{ scope.row.actual_amount || 0 }}
+ <template #default="scope"> ৳{{ scope.row.actual_amount || 0 }} </template>
<el-table-column label="提款方式" min-width="120">
@@ -80,18 +74,18 @@
<el-table-column fixed="right" label="操作" min-width="150">
@click="approveWithdraw(scope.row)"
通过
</el-button>
+ type="danger"
@click="rejectWithdraw(scope.row)"
拒绝
@@ -117,7 +111,7 @@
@@ -158,7 +152,7 @@
@@ -169,12 +163,12 @@
+ const { error, data } = await api.withdraw.list({
class: 'danger',
function editRow(row) {
useModal(
withdrawEdit,
@@ -231,7 +225,7 @@
// 通过提款
async function approveWithdraw(row) {
ElMessageBox.confirm('确认通过该笔提款申请?', '提示', {
@@ -239,14 +233,14 @@
- const { error } = await api.approve(row.id);
+ const { error } = await api.withdraw.approve(row.id);
ElMessage.success('通过成功');
// 拒绝提款
async function rejectWithdraw(row) {
ElMessageBox.prompt('请输入拒绝原因', '拒绝提款', {
@@ -255,14 +249,14 @@
inputPattern: /.+/,
inputErrorMessage: '拒绝原因不能为空',
}).then(async ({ value }) => {
- const { error } = await api.reject(row.id, { reason: value });
+ const { error } = await api.withdraw.reject(row.id, { reason: value });
ElMessage.success('拒绝成功');
@@ -275,7 +269,7 @@
- const { error } = await api.batchApprove(ids.join(','));
+ const { error } = await api.withdraw.batchApprove(ids.join(','));
ElMessage.success('批量通过成功');
@@ -289,7 +283,7 @@
- const { error } = await api.batchReject(ids.join(','), { reason: value });
+ const { error } = await api.withdraw.batchReject(ids.join(','), { reason: value });
ElMessage.success('批量拒绝成功');
- path: 'withdraw',
- name: 'shop.admin.finance.withdraw',
- component: () => import('@/app/shop/admin/finance/withdraw/index.vue'),
- title: '提款',
- ...CRUD('shop/admin/withdraw'),
- select: (params) => SELECT('shop/admin/withdraw', params),
- approve: (id) => ({
- msg: '通过成功',
- reject: (id, data) => ({
- msg: '拒绝成功',
- batchApprove: (ids) => ({
- msg: '批量通过成功',
- batchReject: (ids, data) => ({
- msg: '批量拒绝成功',
@@ -19,7 +19,7 @@
- import { api } from './category.service';
+ import { api } from '../goods.service';
@@ -42,7 +42,7 @@
+ const { error, data } = await api.category.detail(id);
@@ -53,8 +53,8 @@
+ ? await api.category.add(submitForm)
+ : await api.category.edit(props.modal.params.id, submitForm);
@@ -89,7 +89,7 @@
+ const { error, data } = await api.category.list({
@@ -184,7 +184,7 @@
// 删除api 单独批量可以直接调用
async function deleteApi(id) {
+ await api.category.delete(id);
@@ -0,0 +1,129 @@
+<template>
+ <el-container class="category-select-view">
+ <el-header class="sa-header">
+ <div class="sa-title sa-flex sa-row-between">
+ <div class="label sa-flex">选择分类</div>
+ <div>
+ <el-button @click="handleCancel">取消</el-button>
+ <el-button type="primary" @click="handleConfirm">确定</el-button>
+ </div>
+ </el-header>
+ <el-main class="sa-p-0">
+ <div class="category-list">
+ <el-tree
+ ref="treeRef"
+ :data="categoryList"
+ :props="treeProps"
+ node-key="id"
+ :default-checked-keys="selectedIds"
+ show-checkbox
+ :check-strictly="checkStrictly"
+ @check="handleCheck"
+ />
+ </el-main>
+ </el-container>
+</template>
+<script>
+ export default {
+ name: 'CategorySelect',
+ };
+</script>
+<script setup>
+ import { ref, reactive, onMounted } from 'vue';
+ const emit = defineEmits(['modalCallBack']);
+ const props = defineProps({
+ modal: {
+ type: Object,
+ default: () => ({}),
+ multiple: {
+ type: Boolean,
+ default: true,
+ checkStrictly: {
+ default: false,
+ });
+ const treeRef = ref();
+ const categoryList = ref([]);
+ const selectedIds = ref([]);
+ const treeProps = {
+ children: 'children',
+ label: 'name',
+ value: 'id',
+ // 获取分类数据
+ async function getData() {
+ try {
+ const { data, error } = await api.category.list();
+ if (error === 0) {
+ categoryList.value = data || [];
+ }
+ } catch (error) {
+ console.error('获取分类数据失败:', error);
+ // 处理选择
+ function handleCheck(data, checked) {
+ if (props.multiple) {
+ selectedIds.value = checked.checkedKeys;
+ } else {
+ selectedIds.value = [data.id];
+ // 单选模式下,取消其他选择
+ treeRef.value.setCheckedKeys([data.id]);
+ // 确认选择
+ function handleConfirm() {
+ const checkedNodes = treeRef.value.getCheckedNodes();
+ const selectedCategories = checkedNodes.map((node) => ({
+ id: node.id,
+ name: node.name,
+ }));
+ emit('modalCallBack', {
+ type: 'confirm',
+ data: props.multiple ? selectedCategories : selectedCategories[0] || null,
+ // 取消选择
+ function handleCancel() {
+ type: 'cancel',
+ onMounted(() => {
+ getData();
+ // 设置默认选中
+ if (props.modal.selectedIds) {
+ selectedIds.value = Array.isArray(props.modal.selectedIds)
+ ? props.modal.selectedIds
+ : [props.modal.selectedIds];
+<style lang="scss" scoped>
+ .category-select-view {
+ .category-list {
+ padding: 20px;
+ height: 400px;
+ overflow-y: auto;
+</style>
@@ -18,6 +18,14 @@ const route = {
title: '商品库',
+ path: 'category',
+ name: 'shop.admin.goods.category',
+ component: () => import('./category/index.vue'),
+ title: '商品分类',
],
@@ -41,6 +49,10 @@ const api = {
+ category: {
+ ...CRUD('shop/admin/category_tag'),
+ select: (params) => SELECT('shop/admin/category_tag', params),
@@ -900,13 +900,10 @@
// 获取分类数据
const getCategoryData = async () => {
try {
- const response = await api.goods.getCategory();
+ const response = await api.goods.getType();
if (response.error === 0) {
// 只使用一级分类
- categoryOptions.value = response.data.map((item) => ({
- id: item.id,
- name: item.name,
+ categoryOptions.value = response.data.categories;
} catch (error) {
console.error('获取分类失败:', error);
@@ -136,7 +136,7 @@
*/
import { nextTick, onMounted, reactive, ref, watch } from 'vue';
import { api } from '../goods.service';
- import { api as categoryApi } from '@/app/shop/admin/category/category.service';
+ import { api as categoryApi } from '@/app/shop/admin/goods/category/category.service';
import { composeFilter } from '@/sheep/utils';
import { useDebounceFn } from '@vueuse/core';
@@ -9,22 +9,22 @@
<el-input v-model="form.model.goods_id" placeholder="请填写商品ID"></el-input>
<el-form-item label="拼团人数" prop="people_num">
- v-model="form.model.people_num"
- :min="2"
+ v-model="form.model.people_num"
+ :min="2"
placeholder="拼团人数"
- <span style="margin-left: 10px;">人</span>
+ <span style="margin-left: 10px">人</span>
<el-form-item label="拼团价格" prop="group_price">
- v-model="form.model.group_price"
+ v-model="form.model.group_price"
placeholder="拼团价格"
<el-form-item label="开始时间" prop="start_time">
<el-date-picker
@@ -51,9 +51,9 @@
<el-form-item label="活动描述" prop="description">
- v-model="form.model.description"
+ v-model="form.model.description"
placeholder="请填写活动描述"
@@ -69,7 +69,7 @@
- import { api } from './group.service';
+ import { api } from '../marketing.service';
@@ -103,7 +103,7 @@
+ const { error, data } = await api.group.detail(id);
@@ -114,8 +114,8 @@
+ ? await api.group.add(submitForm)
+ : await api.group.edit(props.modal.params.id, submitForm);
- path: 'group',
- name: 'shop.admin.marketing.group',
- component: () => import('@/app/shop/admin/marketing/group/index.vue'),
- title: '拼团',
- ...CRUD('shop/admin/group_buy'),
- select: (params) => SELECT('shop/admin/group_buy', params),
@@ -52,14 +52,10 @@
<el-table-column label="拼团人数" min-width="100">
- {{ scope.row.people_num || '-' }}人
+ <template #default="scope"> {{ scope.row.people_num || '-' }}人 </template>
<el-table-column label="拼团价格" min-width="120">
- ৳{{ scope.row.group_price || '-' }}
+ <template #default="scope"> ৳{{ scope.row.group_price || '-' }} </template>
@@ -113,7 +109,7 @@
@@ -145,7 +141,7 @@
+ const { error, data } = await api.group.list({
@@ -208,7 +204,7 @@
+ await api.group.delete(id);
@@ -227,7 +223,7 @@
break;
default:
- await api.edit(ids.join(','), {
+ await api.group.edit(ids.join(','), {
status: type,
@@ -0,0 +1,31 @@
+ path: 'marketing',
+ name: 'shop.admin.marketing',
+ title: '营销',
+ path: 'group',
+ name: 'shop.admin.marketing.group',
+ component: () => import('./group/index.vue'),
+ title: '拼团',
+ // 拼团相关 API
+ ...CRUD('shop/admin/group_buy'),
+ select: (params) => SELECT('shop/admin/group_buy', params),
@@ -103,7 +103,6 @@
<div class="order-sn sa-flex">
订单号:{{ scope.row.order_sn }}
<el-button
- v-auth="'shop.admin.order.order.detail'"
class="is-link sa-m-l-4"
type="primary"
size="small"
@@ -1,6 +1,6 @@
import Content from '@/sheep/layouts/content.vue';
import { request } from '@/sheep/request';
import { EXPORT } from '@/sheep/request/export';
const route = {
@@ -12,8 +12,8 @@ const route = {
children: [
- path: 'order',
- name: 'shop.admin.order.order',
+ path: 'list',
+ name: 'shop.admin.order.list',
component: () => import('@/app/shop/admin/order/order/index.vue'),
meta: {
title: '订单管理',
@@ -35,6 +35,14 @@ const route = {
title: '订单发票',
+ path: 'setting',
+ name: 'shop.admin.order.setting',
+ component: () => import('@/app/shop/admin/order/setting/index.vue'),
+ title: '订单设置',
@@ -186,6 +194,10 @@ const api = {
+ setting: {
+ ...CRUD('shop/admin/order/setting'),
+ select: (params) => SELECT('shop/admin/order/setting', params),
@@ -342,6 +342,7 @@
<style lang="scss" scoped>
.order-detail {
.order-track {
h3 {
font-size: 16px;
@@ -0,0 +1,1251 @@
+ <el-container class="order-detail">
+ <el-main>
+ <div class="warm-tip sa-m-b-16">
+ 温馨提示
+ <br />1、如果无法发货,请及时与买家联系并说明情况后主动退款;
+ <br />2、买家申请售后,须征得买家同意后发货,否则买家有权拒收货物;
+ <br />3、订单全部退款将会退回商品库存,并且减少实际销量,订单商品上的主动退款不会退回库存和销量
+ <div class="status-content sa-m-b-16">
+ <el-row :gutter="10">
+ <el-col class="sa-col-12 left" :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+ <div class="name sa-flex sa-m-b-4">
+ <el-icon class="refresh sa-m-r-4" @click="getOrderDetail"><RefreshLeft /></el-icon>
+ {{ state.orderDetail.status_text }}
+ <div class="desc sa-m-b-16">
+ {{ state.orderDetail.status_desc }}
+ <div class="tools sa-m-b-4">
+ <template v-if="state.orderDetail.btns">
+ v-if="state.orderDetail.btns.includes('send')"
+ v-auth="'shop.admin.order.order.dispatch'"
+ type="primary"
+ @click="onOpenDispatch"
+ >立即发货</el-button
+ v-if="state.orderDetail.btns.includes('refund')"
+ v-auth="'shop.admin.order.order.fullrefund'"
+ @click="onOpenRefund"
+ >全部退款</el-button
+ </template>
+ <div class="memo sa-flex">
+ <template v-if="memoForm.flag">
+ <el-input class="sa-w-120 sa-m-r-12" v-model="memoForm.data" size="small" />
+ <el-button class="is-link" type="primary" size="small" @click="onConfirmMemo"
+ >确定</el-button
+ size="small"
+ @click="memoForm.flag = false"
+ >取消</el-button
+ <!-- TODO:这里没有权限的时候有问题 -->
+ <template v-if="!memoForm.flag">
+ <div v-if="state.orderDetail.memo" class="sa-m-r-12">
+ {{ state.orderDetail.memo }}
+ v-auth="'shop.admin.order.order.editmemo'"
+ @click="onChangeMemoFlag(state.orderDetail.memo)"
+ >{{ state.orderDetail.memo ? '修改' : '添加' }}备注</el-button
+ </el-col>
+ <el-col class="sa-col-24 center" :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+ <status-steps :orderDetail="state.orderDetail" />
+ <el-col class="sa-col-12 right" :xs="12" :sm="12" :md="6" :lg="6" :xl="6">
+ <div class="right-item">
+ <div class="label">商品总价:</div>
+ <div class="content">¥{{ state.orderDetail.goods_amount }}</div>
+ <div class="label">运费价格:</div>
+ <div class="content"> ¥{{ state.orderDetail.dispatch_amount }} </div>
+ <div
+ class="right-item right-item-discount"
+ v-if="
+ state.orderDetail.ext &&
+ (state.orderDetail.ext.promo_infos || state.orderDetail.coupon_id)
+ "
+ <div class="label">活动优惠:</div>
+ <div class="content">
+ class="sa-flex sa-m-b-12"
+ v-if="Number(state.orderDetail.ext.promo_discounts.full_reduce) > 0"
+ <div class="label">满减</div>
+ <div class="content sa-m-l-12">
+ -¥{{ state.orderDetail.ext.promo_discounts.full_reduce }}
+ v-if="Number(state.orderDetail.ext.promo_discounts.full_discount) > 0"
+ <div class="label">满折</div>
+ -¥{{ state.orderDetail.ext.promo_discounts.full_discount }}
+ v-if="Number(state.orderDetail.ext.promo_discounts.full_gift) > 0"
+ <div class="label">满赠</div>
+ <div class="content sa-m-l-12"></div>
+ v-if="Number(state.orderDetail.ext.promo_discounts.free_shipping) > 0"
+ <div class="label">满包邮</div>
+ -¥{{ state.orderDetail.ext.promo_discounts.free_shipping }}
+ <div v-if="state.orderDetail.coupon_id" class="right-item">
+ <div class="label">优惠券</div>
+ -¥{{ state.orderDetail.coupon_discount_fee }}
+ <div class="label">
+ {{
+ ['paid', 'completed'].includes(state.orderDetail.status)
+ ? '实付金额'
+ : '应付金额'
+ }}:
+ <div class="content fee-content sa-flex">
+ ¥{{ state.orderDetail.pay_fee }}
+ <s
+ v-if="state.orderDetail.pay_fee != state.orderDetail.original_pay_fee"
+ class="original-pay-fee sa-m-l-4"
+ {{ state.orderDetail.original_pay_fee }}
+ </s>
+ v-if="state.orderDetail.btns && state.orderDetail.btns.includes('change_fee')"
+ v-auth="'shop.admin.order.order.changefee'"
+ class="is-link sa-m-l-8"
+ @click="onChangeFee"
+ >改价</el-button
+ </el-row>
+ <div class="remark right-item">
+ <div class="label">买家留言:</div>
+ {{ state.orderDetail.remark || '暂无留言' }}
+ <!-- tabs内容 -->
+ <div class="tabs-content sa-m-b-16">
+ <el-tabs v-model="state.orderTabs" @tab-change="state.changeExpressStatus = false">
+ <el-tab-pane label="订单信息" name="1">
+ <el-row class="order-content" :gutter="10">
+ <el-col class="sa-col-12" :xs="24" :sm="24" :md="12" :lg="6" :xl="6">
+ <div class="title">交易信息</div>
+ <div class="item">
+ <div class="label">订单编号:</div>
+ {{ state.orderDetail.order_sn }}
+ <sa-svg
+ class="copy sa-m-l-4 cursor-pointer"
+ name="sa-copy"
+ @click="useClip(state.orderDetail.order_sn)"
+ ></sa-svg>
+ <div class="label">订单来源:</div>
+ {{ state.orderDetail.platform }}
+ <div class="item" v-if="state.orderDetail.paid_time">
+ <div class="label">付款时间:</div>
+ <div class="content">{{ state.orderDetail.paid_time }}</div>
+ <el-col
+ v-if="state.orderDetail.user"
+ class="sa-col-12"
+ :xs="24"
+ :sm="24"
+ :md="12"
+ :lg="6"
+ :xl="6"
+ <div class="title">买家信息</div>
+ <div class="label">用户昵称:</div>
+ <div class="content sa-flex">
+ <sa-user-profile
+ :user="state.orderDetail.user"
+ :id="state.orderDetail.user_id"
+ :isAvatar="false"
+ v-if="state.orderDetail.address"
+ <div class="title sa-flex">
+ 收货信息
+ <template v-if="state.orderDetail?.btns.includes('edit_consignee')">
+ <template v-if="!state.editConsigneeFlag">
+ v-auth="'shop.admin.order.order.editconsignee'"
+ class="is-link sa-m-l-12"
+ @click="onChangeEditConsigneeFlag(true)"
+ >修改</el-button
+ @click="
+ useClip(
+ `收货昵称:${state.orderDetail.address.consignee};联系方式:${state.orderDetail.address.mobile};
+ 收货地址:${state.orderDetail.address.province_name}${state.orderDetail.address.city_name}${state.orderDetail.address.district_name}${state.orderDetail.address.address}`,
+ )
+ >复制</el-button
+ <template v-if="state.editConsigneeFlag">
+ @click="state.editConsigneeFlag = false"
+ 取消
+ </el-button>
+ @click="onEditConsignee()"
+ <div class="label">收货昵称:</div>
+ <span v-if="!state.editConsigneeFlag">
+ {{ state.orderDetail.address.consignee }}
+ </span>
+ v-if="state.editConsigneeFlag"
+ v-model="state.address.consignee"
+ <div class="label">联系方式:</div>
+ {{ state.orderDetail.address.mobile }}
+ v-model="state.address.mobile"
+ <div class="label">收货地址:</div>
+ <div class="sa-m-b-4" v-if="!state.editConsigneeFlag">
+ {{ state.orderDetail.address.province_name }}
+ {{ state.orderDetail.address.city_name }}
+ {{ state.orderDetail.address.district_name }}
+ <el-cascader
+ class="sa-m-b-4"
+ ref="addressRef"
+ v-model="state.address.pcd"
+ :options="state.area"
+ :props="{ label: 'name', value: 'id' }"
+ <div v-if="!state.editConsigneeFlag">
+ {{ state.orderDetail.address.address }}
+ v-model="state.address.address"
+ v-if="state.orderDetail.invoice_status == 1"
+ 发票信息
+ <div class="sa-m-l-12">
+ <span
+ v-if="state.orderDetail.invoice.status != 'waiting'"
+ class="invoice-status"
+ >{{ state.orderDetail.invoice.status_text }}</span
+ v-if="state.orderDetail.invoice.status == 'waiting'"
+ text
+ @click="onChangeInvoiceStatus(state.orderDetail.invoice)"
+ >确认开具</el-button
+ <div class="label">发票类型:</div>
+ {{ state.orderDetail.invoice.type_text }}
+ <div class="label">抬头名称:</div>
+ {{ state.orderDetail.invoice.name }}
+ <div class="item" v-if="state.orderDetail.invoice.type === 'company'">
+ <div class="label">税号:</div>
+ {{ state.orderDetail.invoice.tax_no }}
+ <div class="label">手机号:</div>
+ {{ state.orderDetail.invoice.mobile }}
+ <div class="label">金额:</div>
+ <div class="content"> ¥{{ state.orderDetail.invoice.amount }} </div>
+ <div class="item" v-if="state.orderDetail.invoice.status === 'finish'">
+ <div class="label">实际开票金额:</div>
+ <div class="content"> ¥{{ state.orderDetail.invoice.invoice_amount }} </div>
+ </el-tab-pane>
+ <el-tab-pane
+ :label="`包裹${exindex + 1}`"
+ v-for="(ex, exindex) in state.orderDetail.express"
+ :key="ex"
+ :name="`${exindex + 2}`"
+ <el-row class="express-content" :gutter="10">
+ <el-col class="sa-col-8 left" :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
+ 物流信息
+ <div class="sa-flex sa-m-l-12">
+ <!-- 运单信息 -->
+ <template v-if="!state.changeExpressStatus">
+ @click="onChangeExpressStatus(ex)"
+ >修改运单</el-button
+ <el-popconfirm
+ width="fit-content"
+ confirm-button-text="确认"
+ cancel-button-text="取消"
+ title="您确定要取消运单吗?"
+ @confirm="onCancelExpress(ex.id)"
+ <template #reference>
+ @click="cancelExpressPopover = true"
+ 取消运单
+ </el-popconfirm>
+ <!-- 修改运单信息 -->
+ <template v-else>
+ @click="state.changeExpressStatus = false"
+ @click="onConfirmExpress(ex.id)"
+ >确认</el-button
+ <div class="label">快递公司:</div>
+ <div v-if="!state.changeExpressStatus">
+ {{ ex.express_name }}
+ v-if="state.changeExpressStatus"
+ v-model="express.code"
+ placeholder="请选择快递公司"
+ @change="onChangeExpressCode"
+ filterable
+ remote
+ reserve-keyword
+ :remote-method="remoteMethod"
+ :loading="deliverCompany.loading"
+ <el-option
+ v-for="dc in deliverCompany.data"
+ :key="dc"
+ :label="dc.name"
+ :value="dc.code"
+ :ref="`dc-${dc.code}`"
+ :data-name="dc.name"
+ >{{ dc.name }} ({{ dc.code }})</el-option
+ <div class="sa-p-l-15 sa-p-r-27">
+ <sa-pagination
+ layout="total, prev, pager, next"
+ :pageData="deliverCompany.pageData"
+ @updateFn="getDeliverCompany"
+ </el-select>
+ <div class="label">快递单号:</div>
+ {{ ex.express_no }}
+ v-model="express.no"
+ <div class="goods-item sa-flex sa-m-t-12" v-for="goods in ex.items" :key="goods">
+ <sa-image :url="goods.goods_image" size="40"></sa-image>
+ <div class="sa-m-l-8">
+ <div class="goods-title sa-table-line-1 sa-m-b-4">
+ {{ goods.goods_title }}
+ <div class="sku sa-flex">
+ <div v-if="goods.goods_sku_text" class="sa-m-r-8">
+ {{ goods.goods_sku_text }}
+ <div>x {{ goods.goods_num }}</div>
+ class="sa-col-16 right"
+ :md="16"
+ :lg="16"
+ :xl="16"
+ <div class="title sa-flex sa-row-between">
+ <template class="sa-flex">
+ 物流状态
+ v-auth="'shop.admin.order.order.updateexpress'"
+ @click="onUpdateExpress(ex.id, 'subscribe')"
+ >重新订阅</el-button
+ <el-popover
+ placement="top"
+ :width="300"
+ trigger="hover"
+ content="如果长时间物流状态没有更新,可以尝试刷新一下。如果没有物流信息,可以尝试重新订阅一下!"
+ <el-icon class="warning sa-m-l-4">
+ <Warning />
+ </el-icon>
+ </el-popover>
+ class="refresh"
+ icon="RefreshRight"
+ @click="onUpdateExpress(ex.id, 'search')"
+ ></el-button>
+ <el-timeline>
+ <el-timeline-item
+ :class="index == 0 ? 'el-timeline-item-first' : ''"
+ v-for="(log, index) in ex.logs"
+ :key="index"
+ :timestamp="log.change_date"
+ :color="index == 0 ? 'var(--t-color-primary)' : ''"
+ <div class="log-content">
+ {{ log.content }}
+ <div class="change-date sa-m-t-8">{{ log.change_date }}</div>
+ </el-timeline-item>
+ </el-timeline>
+ </el-tabs>
+ <!-- 商品列表 -->
+ <div class="goods-content">
+ <el-tabs>
+ <el-tab-pane label="商品信息">
+ <el-table class="sa-table" :data="state.orderDetail.items" stripe>
+ <el-table-column label="商品信息" min-width="250">
+ <template #default="scope">
+ <goods-item
+ :goods="{
+ id: scope.row.goods_id,
+ image: scope.row.goods_image,
+ title: scope.row.goods_title,
+ sku_text: scope.row.goods_sku_text,
+ }"
+ :isGoods="true"
+ mode="order-detail"
+ ></goods-item>
+ </el-table-column>
+ <el-table-column prop="goods_price" label="单价(元)" min-width="100" />
+ <el-table-column prop="goods_num" label="数量(件)" min-width="80" />
+ <el-table-column prop="discount_fee" label="优惠(元)" min-width="90" />
+ <el-table-column prop="pay_fee" label="支付金额" min-width="120" />
+ <el-table-column label="订单类型" min-width="100">
+ <template #default>{{ state.orderDetail.type_text }}</template>
+ <el-table-column prop="dispatch_type_text" label="配送方式" width="100">
+ <el-table-column label="发货状态" min-width="120">
+ <status-button
+ from="detail"
+ :order="state.orderDetail"
+ type="dispatch"
+ :item="scope.row"
+ ></status-button>
+ <el-table-column label="退款状态" min-width="100">
+ type="refund"
+ @updateList="getOrderDetail"
+ <el-table-column label="售后状态" min-width="120">
+ <status-button from="detail" type="aftersale" :item="scope.row"></status-button>
+ <el-table-column label="商品评价" min-width="100">
+ type="comment"
+ <el-table-column width="100" label="配送信息">
+ <div v-if="state.orderDetail.dispatch_status != 0">
+ <el-popover trigger="hover" width="220">
+ scope.row.dispatch_type == 'autosend' ||
+ scope.row.dispatch_type == 'custom'
+ <span>发货内容:</span>
+ <template v-if="scope.row.ext?.dispatch_content_type == 'text'">
+ {{ scope.row.ext?.dispatch_content }}
+ <template
+ v-if="scope.row.ext?.dispatch_content_type == 'params'"
+ v-for="item in scope.row.ext?.dispatch_content"
+ {{ item.title }}-{{ item.content }}
+ <template v-if="scope.row.dispatch_type != 'express'" #reference>
+ <el-button type="primary" link>查看详情</el-button>
+ <div v-if="scope.row.dispatch_type == 'express'">-</div>
+ <div v-else>-</div>
+ </el-table>
+ <el-tab-pane v-if="state.orderDetail?.activity_orders?.length > 0" label="优惠信息">
+ <el-table class="sa-table" :data="state.orderDetail.activity_orders" stripe>
+ <el-table-column prop="id" label="ID" min-width="80" />
+ <el-table-column label="活动标题" min-width="120">
+ <div class="sa-table-line-1">
+ {{ scope.row.activity_title }}
+ <el-table-column label="活动类型" min-width="120" align="center">
+ :style="
+ activityStatusStyle(scope.row.activity_type, {
+ padding: '8px 12px',
+ 'border-radius': '15px',
+ })
+ {{ scope.row.activity_type_text }}
+ <el-table-column label="优惠信息" min-width="120" align="center">
+ {{ scope.row.discount_text }}
+ <el-table-column
+ prop="discount_fee"
+ label="优惠金额(元)"
+ min-width="120"
+ align="center"
+ prop="goods_amount"
+ label="参与商品金额(元)"
+ min-width="140"
+ <el-table-column label="参与商品" min-width="250" align="center">
+ <el-popover placement="top-start" :width="300" trigger="hover">
+ <div class="discount-goods">
+ <div class="item sa-flex" v-for="goods in scope.row.items" :key="goods">
+ <div class="">#{{ goods.goods_id }}</div>
+ <span class="discount-items">
+ {{ scope.row.items?.length }}
+ 件商品
+ <el-table-column prop="create_time" label="参与时间" min-width="172" />
+ <!-- 支付列表 -->
+ <div class="sa-title">支付信息</div>
+ <div class="pay-content">
+ <el-table class="sa-table" :data="state.orderDetail.pays" stripe>
+ <el-table-column prop="pay_sn" label="支付单号" min-width="250" />
+ <el-table-column prop="pay_type_text" label="支付方式" min-width="80" align="center" />
+ <el-table-column prop="pay_fee" label="支付金额" min-width="120" align="center" />
+ <el-table-column prop="transaction_id" label="交易单号" min-width="280" align="center">
+ <span>{{ scope.row.transaction_id || '-' }}</span>
+ <el-table-column label="支付状态" min-width="120" align="center">
+ <span :class="['status', `status-${scope.row.status}`]">{{
+ scope.row.status_text
+ }}</span>
+ <el-table-column prop="refund_fee" label="已退款金额" min-width="120" align="center" />
+ <el-table-column prop="create_time" label="交易时间" min-width="172" align="center" />
+ import { computed, onMounted, reactive, ref, unref } from 'vue';
+ import { api } from '../order.service';
+ import { api as dataApi } from '@/app/shop/admin/data/data.service';
+ import { useModal } from '@/sheep/hooks';
+ import useClip from '@/sheep/utils/clipboard.js';
+ import useExpress from '@/app/shop/admin/data/express/express.js';
+ import StatusButton from './components/status.vue';
+ import { activityStatusStyle } from './status.js';
+ import GoodsItem from '@/app/shop/components/goods-item.vue';
+ import OrderDispatch from './dispatch.vue';
+ import OrderRefund from './refund.vue';
+ import OrderFee from './fee.vue';
+ import StatusSteps from './components/status-steps.vue';
+ import InvoiceEdit from '../invoice/edit.vue';
+ import { cloneDeep } from 'lodash';
+ const props = defineProps(['modal']);
+ const state = reactive({
+ orderDetail: {}, // 订单详情
+ orderStep: 1,
+ orderTabs: '1',
+ changeExpressStatus: false, // 变更运单状态
+ editConsigneeFlag: false,
+ address: {},
+ area: [],
+ // 获取订单详情
+ async function getOrderDetail() {
+ const { error, data } = await api.order.detail(props.modal.params.id);
+ state.orderDetail = data;
+ state.orderTabs = '1';
+ onChangeEditConsigneeFlag(false);
+ changeOrderStep();
+ async function getArea() {
+ const { data } = await dataApi.area.select();
+ state.area = data;
+ function changeOrderStep() {
+ if (state.orderDetail.status == 'unpaid') {
+ state.orderStep = 1;
+ } else if (state.orderDetail.status == 'paid') {
+ state.orderStep = 2;
+ switch (state.orderDetail.status_code) {
+ case 'nosend':
+ break;
+ case 'noget':
+ state.orderStep = 3;
+ case 'nocomment':
+ state.orderStep = 4;
+ case 'commented':
+ } else if (state.orderDetail.status == 'completed') {
+ state.orderStep = 5;
+ function onOpenDispatch() {
+ useModal(
+ OrderDispatch,
+ title: '订单发货',
+ data: state.orderDetail,
+ confirm: () => {
+ getOrderDetail();
+ );
+ function onOpenRefund() {
+ OrderRefund,
+ title: '全部退款',
+ class: 'refund-dialog',
+ type: 'order.fullRefund',
+ data: {
+ id: state.orderDetail.id,
+ money: state.orderDetail.pay_fee,
+ function onChangeFee() {
+ OrderFee,
+ title: '改价',
+ class: 'fee-dialog',
+ pay_fee: state.orderDetail.pay_fee,
+ // 备注
+ const memoForm = reactive({
+ flag: false,
+ data: '',
+ function onChangeMemoFlag(val) {
+ memoForm.flag = true;
+ memoForm.data = val;
+ async function onConfirmMemo() {
+ const { error } = await api.order.editMemo(state.orderDetail.id, {
+ memo: memoForm.data,
+ if (error == 0) {
+ memoForm.flag = false;
+ const { express, deliverCompany, getDeliverCompany, onChangeExpressCode, remoteMethod } =
+ useExpress();
+ function onChangeExpressStatus(ex) {
+ state.changeExpressStatus = true;
+ express.name = ex.express_name;
+ express.code = ex.express_code;
+ express.no = ex.express_no;
+ async function onChangeInvoiceStatus(row) {
+ InvoiceEdit,
+ title: '编辑',
+ type: 'edit',
+ data: row,
+ // const { data } = await api.invoice.confirm(id);
+ // 确认修改运单
+ async function onConfirmExpress(order_express_id) {
+ await api.order.dispatch({
+ order_id: props.modal.params.id,
+ order_express_id,
+ action: 'change',
+ express: express,
+ state.changeExpressStatus = false;
+ // 取消运单
+ const cancelExpressPopover = ref(false);
+ async function onCancelExpress(order_express_id) {
+ action: 'cancel',
+ async function onUpdateExpress(order_express_id, type) {
+ const { error } = await api.order.updateExpress(order_express_id, { type });
+ if (error == 0) getOrderDetail();
+ // 修改收货信息
+ function onChangeEditConsigneeFlag(type) {
+ if (state.orderDetail.address) {
+ state.address = {
+ consignee: state.orderDetail.address.consignee,
+ mobile: state.orderDetail.address.mobile,
+ pcd: [
+ state.orderDetail.address.province_id,
+ state.orderDetail.address.city_id,
+ state.orderDetail.address.district_id,
+ address: state.orderDetail.address.address,
+ state.editConsigneeFlag = type;
+ const addressRef = ref();
+ async function onEditConsignee() {
+ let label = unref(addressRef).getCheckedNodes()[0].pathLabels;
+ let tempAddress = cloneDeep(state.address);
+ tempAddress = {
+ ...tempAddress,
+ province_name: label[0],
+ province_id: state.address.pcd[0],
+ city_name: label[1],
+ city_id: state.address.pcd[1],
+ district_name: label[2],
+ district_id: state.address.pcd[2],
+ delete tempAddress.pcd;
+ await api.order.editConsignee(state.orderDetail.id, tempAddress);
+ state.editConsigneeFlag = false;
+ getArea();
+ .order-detail {
+ .warm-tip {
+ line-height: 16px;
+ font-size: 12px;
+ color: var(--sa-font);
+ .status-content {
+ border-radius: 8px;
+ padding: var(--sa-padding);
+ background: var(--sa-table-header-bg);
+ .left,
+ .center,
+ .right {
+ margin-bottom: var(--sa-padding);
+ .right-item {
+ margin-bottom: 8px;
+ display: flex;
+ align-items: center;
+ &.right-item-discount {
+ align-items: flex-start;
+ margin-bottom: 0;
+ .label {
+ flex-shrink: 0;
+ color: var(--sa-subfont);
+ .content {
+ color: var(--sa-subtitle);
+ .left {
+ .name {
+ line-height: 24px;
+ font-size: 18px;
+ color: var(--sa-title);
+ font-weight: 900;
+ .refresh {
+ color: var(--t-color-primary);
+ cursor: pointer;
+ .desc {
+ .tools {
+ height: 32px;
+ .memo {
+ height: 24px;
+ font-weight: 400;
+ .fee-content {
+ height: 16px;
+ .original-pay-fee {
+ color: #999;
+ .remark {
+ line-height: 32px;
+ padding: 0 12px;
+ background: var(--sa-table-striped);
+ border-radius: 4px;
+ .tabs-content {
+ :deep() {
+ .el-tabs {
+ .el-tabs__content {
+ padding: var(--sa-padding) var(--sa-padding) 0;
+ .title {
+ font-size: 14px;
+ font-weight: 600;
+ overflow: hidden;
+ .warning {
+ color: #faad14;
+ .item {
+ margin-bottom: 4px;
+ &:last-child {
+ .copy {
+ color: var(--el-color-primary);
+ .order-content {
+ .el-col {
+ padding-bottom: var(--sa-padding);
+ .invoice-status {
+ .nickname {
+ .express-content {
+ max-height: 218px;
+ overflow: auto;
+ .goods-item {
+ .goods-title {
+ font-weight: 500;
+ .goods-title,
+ .sku {
+ padding: 5px;
+ background-color: transparent;
+ .el-timeline-item__timestamp {
+ position: absolute;
+ top: 2px;
+ left: -140px;
+ margin-top: 0;
+ .goods-content {
+ .pay-content {
+ .status {
+ &.status-unpaid {
+ &.status-paid {
+ color: #52c41a;
+ &.status-refund {
+ color: #ff4d4f;
+ .el-tabs__nav-wrap::after {
+ height: 0;
+ .discount-items {
+ .discount-goods {
+ padding: 8px 0;
+ border-bottom: 1px solid var(--sa-border);
+ &:last-of-type {
+ border-bottom: 0;
+ .log-content {
+ line-height: 18px;
+ .change-date {
+ line-height: 14px;
+ .el-timeline-item-first {
+ &::after {
+ content: '';
+ top: 20px;
+ left: 4px;
+ width: 2px;
+ height: calc(50% - 16px);
+ background: var(--t-color-primary);
+ .el-timeline-item__tail {
+ top: 8px;
+ bottom: 8px;
@@ -0,0 +1,1207 @@
+ <el-container class="order-page panel-block">
+ <el-tabs class="sa-tabs" v-model="filterParams.data.status" @tab-change="onChangeStatus">
+ v-for="(sl, key) in statusList"
+ :key="sl"
+ :label="`${sl.label}${sl.num ? '(' + sl.num + ')' : ''}`"
+ :name="key"
+ ></el-tab-pane>
+ <div class="label sa-flex">
+ <span class="left">订单列表</span>
+ <search-condition
+ :conditionLabel="filterParams.conditionLabel"
+ @deleteFilter="deleteFilter"
+ ></search-condition>
+ v-auth="'shop.admin.order.order.list'"
+ class="sa-button-refresh"
+ @click="getData()"
+ <el-button class="sa-button-refresh" icon="Search" @click="openFilter"></el-button>
+ v-auth="'shop.admin.order.order.export'"
+ :loading="exportLoading"
+ :disabled="exportLoading"
+ @click="onExport('export')"
+ >订单导出</el-button
+ v-if="filterParams.data.status == 'nosend'"
+ v-auth="'shop.admin.order.order.exportdelivery'"
+ @click="onExport('exportDelivery')"
+ >导出发货单</el-button
+ <el-main class="sa-p-0" v-loading="loading">
+ <el-table
+ height="100%"
+ class="sa-table"
+ :data="table.data"
+ :span-method="arraySpanMethod"
+ default-expand-all
+ @selection-change="handleSelectionChange"
+ <template #empty>
+ <sa-empty />
+ <el-table-column type="expand">
+ <template #default="props">
+ class="sa-table sa-expand-table"
+ :data="props.row.items"
+ :span-method="objectSpanMethod"
+ <el-table-column width="88"></el-table-column>
+ <el-table-column width="324">
+ <div class="goods-item sa-flex">
+ <sa-image :url="scope.row.goods_image" size="58" />
+ <div class="right sa-m-l-12">
+ <div class="goods-title sa-flex sa-table-line-1">
+ <span class="goods-id" @click="onOpenGoodsEdit(scope.row.goods_id)"
+ >#{{ scope.row.goods_id }}</span
+ {{ scope.row.goods_title }}
+ <div class="goods-sku-text sa-m-t-10">
+ <span>商品编码:{{ scope.row.goods_sku_price_out_code }}</span>
+ <div class="sa-flex">
+ <span class="goods-price sa-m-r-8">¥{{ scope.row.goods_price }}</span>
+ <span class="goods-num sa-m-r-12">x{{ scope.row.goods_num }}</span>
+ <index-status type="dispatch" :item="scope.row" />
+ class="sa-m-l-8"
+ plain
+ @click="onOpenManualOrder(scope.row.id)"
+ >手动下单</el-button
+ scope.row.order_source === 'zkh' && scope.row.status_code === 'nosend'
+ @click="onOpenZkhSubmitConfirm(scope.row.id)"
+ >震坤行下单</el-button
+ <div class="sa-m-t-8 sa-font-24">
+ <div v-if="scope.row.out_order_id"
+ >外部订单号:{{ scope.row.out_order_id }}</div
+ <div v-if="scope.row.out_order_item_id" class="sa-m-t-5 sa-m-b-5"
+ >外部子订单号:{{ scope.row.out_order_item_id }}</div
+ <div v-if="scope.row.order_source"
+ >下单渠道:{{ formatOrderSource(scope.row.order_source) }}</div
+ <el-table-column min-width="140" align="center">
+ <index-status type="aftersale" :item="scope.row" @updateList="getData" />
+ <el-table-column min-width="92" align="center">
+ <template #default>
+ <sa-user-profile :user="props.row.user" :id="props.row.user_id" mode="col" />
+ <el-table-column min-width="168" align="center">
+ class="address-content sa-flex sa-flex-col sa-col-top"
+ v-if="props.row.address"
+ <div class="consignee sa-m-b-6">
+ <span>{{ props.row.address.consignee }}</span>
+ <span class="sa-m-l-8">{{ props.row.address.mobile }}</span>
+ <div class="address-name sa-m-b-6">
+ {{ props.row.address.province_name }}/{{ props.row.address.city_name }}/{{
+ props.row.address.district_name
+ }}/{{ props.row.address.town_name || '暂无' }}
+ <div class="address sa-table-line-1">
+ {{ props.row.address.address }}
+ <div v-else>{{ props.row.address_id }}</div>
+ <el-table-column prop="dispatch_type_text" min-width="168" align="center">
+ <el-table-column min-width="160" align="center">
+ <div class="sa-flex-col sa-col-center">
+ v-if="scope.row.activity_type"
+ class="activity-type-text sa-m-r-4"
+ :class="scope.row.activity_type"
+ >{{ scope.row.activity_type_text }}</div
+ ¥{{ scope.row.pay_fee }}
+ <index-status
+ v-if="scope.row.refund_status != 0"
+ <template v-if="Number(scope.row.discount_fee)">
+ popper-class="discount-fee-popover"
+ class="promo-type-text"
+ v-for="text in scope.row.promo_types_text"
+ :key="text"
+ >{{ text }}</div
+ <div v-if="scope.row.promo_types_text?.length > 0" class="discount-fee">
+ (优惠: ¥{{ scope.row.discount_fee }})
+ <div v-if="scope.row.promo_types_text?.length == 0" class="discount-fee">
+ (优惠: -¥{{ scope.row.discount_fee }})
+ <el-table-column min-width="240">
+ v-model:visible="refundPopover[props.$index]"
+ popper-class="refund-popover"
+ placement="top-start"
+ :width="204"
+ trigger="click"
+ <el-icon><QuestionFilled /></el-icon>
+ 您同意用户进行申请退款吗?
+ <div class="sa-flex sa-row-right">
+ v-auth="'shop.admin.order.order.applyrefundrefuse'"
+ type="info"
+ @click="onApplyRefundRefuse(props.row.id, props.$index)"
+ >拒绝</el-button
+ @click="onOpenRefund(props.row, props.$index)"
+ >同意</el-button
+ <div class="apply-refund-wrap">
+ v-if="props.row.btns?.includes('apply_refund_oper')"
+ class="apply-refund-button sa-m-r-12"
+ 用户申请退款
+ <el-icon><ArrowRight /></el-icon>
+ v-model:visible="confirmPopover[props.$index]"
+ popper-class="confirm-popover sa-popper"
+ <el-icon>
+ <question-filled />
+ 确认用户是否收货?
+ v-auth="'shop.admin.order.order.offlinerefuse'"
+ link
+ @click="onOfflineRefuse(props.row.id, props.$index)"
+ >用户拒收
+ v-auth="'shop.admin.order.order.offlineconfirm'"
+ @click="onOfflineConfirm(props.row.id, props.$index)"
+ >确认收货
+ v-if="props.row.btns?.includes('confirm')"
+ class="send-button sa-m-r-12"
+ 确认收货
+ <arrow-right />
+ v-if="props.row.btns?.includes('send')"
+ class="send-button"
+ @click="onSend(props.row)"
+ 立即发货
+ v-auth="'shop.admin.order.order.detail'"
+ @click="detailRow(props.row.id)"
+ >详情</el-button
+ v-auth="'shop.admin.order.order.action'"
+ @click="onOpenAction(props.row.id)"
+ >日志</el-button
+ <el-table-column type="selection" width="40" />
+ <el-table-column label="商品信息" width="304">
+ <div class="order-wrap sa-flex">
+ <div class="id">#{{ scope.row.id }}</div>
+ <div class="order-sn sa-flex">
+ 订单号:{{ scope.row.order_sn }}
+ @click="useClip(scope.row.order_sn)"
+ <div class="create-time"> 下单时间:{{ scope.row.create_time }} </div>
+ <div class="pay-types-text">{{ scope.row.pay_types_text?.join(',') }}</div>
+ <el-table-column label="售后状态" min-width="140" align="center"></el-table-column>
+ <el-table-column label="下单用户" min-width="92" align="center"></el-table-column>
+ <el-table-column label="收货地址" min-width="168" align="center"></el-table-column>
+ <el-table-column label="配送方式" min-width="168" align="center"></el-table-column>
+ <el-table-column label="支付信息" min-width="160" align="center">
+ <div class="sa-flex sa-row-center">
+ <el-popover popper-class="pay-fee-popover sa-popper" placement="top" trigger="hover">
+ <div class="pay-fee-item">
+ <div class="content">¥{{ scope.row.goods_amount }}</div>
+ <div class="content"> ¥{{ scope.row.dispatch_amount }} </div>
+ class="pay-fee-item pay-fee-item-discount"
+ v-if="scope.row.ext && (scope.row.ext.promo_infos || scope.row.coupon_id)"
+ class="pay-fee-item"
+ v-if="Number(scope.row.ext.promo_discounts.full_reduce) > 0"
+ <div class="label sa-m-r-8">满减</div>
+ -¥{{ scope.row.ext.promo_discounts.full_reduce }}
+ v-if="Number(scope.row.ext.promo_discounts.full_discount) > 0"
+ <div class="label sa-m-r-8">满折</div>
+ -¥{{ scope.row.ext.promo_discounts.full_discount }}
+ v-if="Number(scope.row.ext.promo_discounts.full_gift) > 0"
+ <div class="label sa-m-r-8">满赠</div>
+ <div class="content"></div>
+ v-if="Number(scope.row.ext.promo_discounts.free_shipping) > 0"
+ <div class="label sa-m-r-8">满包邮</div>
+ -¥{{ scope.row.ext.promo_discounts.free_shipping }}
+ <div v-if="scope.row.coupon_id" class="pay-fee-item">
+ <div class="label sa-m-r-8">优惠券</div>
+ <div class="content"> -¥{{ scope.row.coupon_discount_fee }} </div>
+ <div class="label"
+ >{{
+ ['paid', 'completed'].includes(scope.row.status) ? '实付金额' : '应付金额'
+ }}:</div
+ v-if="scope.row.pay_fee != scope.row.original_pay_fee"
+ {{ scope.row.original_pay_fee }}
+ <div class="pay-fee-reference">¥{{ scope.row.pay_fee }}</div>
+ v-if="scope.row?.btns.includes('change_fee')"
+ @click="onChangeFee(scope.row)"
+ <el-table-column label="操作" min-width="240">
+ <index-status :order="scope.row" type="status_code" />
+ <sa-view-bar>
+ <template #left>
+ <sa-batch-handle
+ :batchHandleTools="batchHandleTools"
+ :selectedLeng="table.selected.length"
+ @batchHandle="batchHandle"
+ ></sa-batch-handle>
+ <template #right>
+ <sa-pagination :pageData="pageData" @updateFn="getData" />
+ </sa-view-bar>
+ name: 'shop.admin.order.order',
+ import { computed, onMounted, reactive, ref } from 'vue';
+ import { ElMessage, ElMessageBox } from 'element-plus';
+ import { useModal, usePagination } from '@/sheep/hooks';
+ import { useSearch } from '@/sheep/components/sa-table/sa-search/useSearch';
+ import { composeFilter } from '@/sheep/utils';
+ import IndexStatus from './components/index-status.vue';
+ import OrderBatchDispatch from './batchDispatch.vue';
+ import OrderDetail from './detail.vue';
+ import OrderAction from './action.vue';
+ import GoodsEdit from '@/app/shop/admin/goods/goods/edit.vue';
+ import { useRoute } from 'vue-router';
+ import ManualOrder from './manualOrder.vue';
+ const route = useRoute();
+ // getType
+ const statusList = reactive({});
+ async function getType() {
+ const { data } = await api.order.getType();
+ for (var key in data) {
+ if (filterParams.tools[key] && key != 'status') {
+ filterParams.tools[key].options.data = data[key];
+ } else if (key == 'pay_type') {
+ filterParams.tools['pay.pay_type'].options.data = data[key];
+ } else if (key == 'status') {
+ data[key].forEach((s) => {
+ statusList[s.type] = { label: s.name, num: 0 };
+ const filterParams = reactive({
+ tools: {
+ status: { label: '订单状态', value: route.query.status || 'all' },
+ order: {
+ type: 'tinputprepend',
+ label: '请输入查询内容',
+ field: 'order',
+ field: 'id',
+ value: '',
+ options: [
+ label: '订单ID',
+ label: '订单编号',
+ value: 'order_sn',
+ label: '售后单号',
+ value: 'aftersale.aftersale_sn',
+ label: '支付单号',
+ value: 'pay.pay_sn',
+ label: '交易流水号',
+ value: 'pay.transaction_id',
+ user: {
+ field: 'user',
+ field: 'user_id',
+ label: '用户ID',
+ value: 'user_id',
+ label: '用户昵称',
+ value: 'user.nickname',
+ label: '用户手机号',
+ value: 'user.mobile',
+ label: '收货人',
+ value: 'address.consignee',
+ label: '收货人手机号',
+ value: 'address.mobile',
+ type: {
+ type: 'tselect',
+ label: '订单类型',
+ field: 'type',
+ data: [],
+ props: {
+ value: 'type',
+ platform: {
+ label: '订单来源',
+ field: 'platform',
+ 'pay.pay_type': {
+ label: '支付方式',
+ field: 'pay.pay_type',
+ activity_type: {
+ label: '活动类型',
+ field: 'activity_type',
+ promo_types: {
+ label: '促销类型',
+ field: 'promo_types',
+ 'item.goods_title': {
+ type: 'tinput',
+ label: '商品名称',
+ field: 'item.goods_title',
+ create_time: {
+ type: 'tdatetimerange',
+ label: '下单时间',
+ field: 'create_time',
+ value: route.query.create_time || [],
+ status: route.query.status || 'all',
+ order: { field: 'id', value: '' },
+ user: { field: 'user_id', value: '' },
+ type: '',
+ platform: '',
+ 'pay.pay_type': '',
+ activity_type: '',
+ promo_types: '',
+ 'item.goods_title': '',
+ create_time: route.query.create_time || [],
+ conditionLabel: {},
+ const { openFilter, deleteFilter } = useSearch({ filterParams, getData });
+ const loading = ref(true);
+ // 表格
+ const table = reactive({
+ order: '',
+ sort: '',
+ selected: [],
+ const { pageData } = usePagination();
+ // 下单渠道格式化函数
+ const formatOrderSource = (source) => {
+ const sourceMap = {
+ self: '自下单',
+ linkedmall: 'linkedmall',
+ zkh: '震坤行',
+ return sourceMap[source] || source;
+ // 获取数据
+ async function getData(page) {
+ loading.value = true;
+ if (page) pageData.page = page;
+ let tempSearch = cloneDeep(filterParams.data);
+ let search = composeFilter(tempSearch, {
+ order_sn: 'like',
+ 'aftersale.aftersale_sn': 'like',
+ 'pay.pay_sn': 'like',
+ 'pay.transaction_id': 'like',
+ 'user.nickname': 'like',
+ 'user.mobile': 'like',
+ 'address.consignee': 'like',
+ 'address.mobile': 'like',
+ promo_types: 'find_in_set',
+ 'item.goods_title': 'like',
+ create_time: 'range',
+ const { error, data } = await api.order.list({
+ page: pageData.page,
+ list_rows: pageData.list_rows,
+ ...search,
+ table.data = data.orders.data;
+ pageData.page = data.orders.current_page;
+ pageData.list_rows = data.orders.per_page;
+ pageData.total = data.orders.total;
+ for (var key in statusList) {
+ statusList[key].num = data[key];
+ loading.value = false;
+ function onChangeStatus(tab) {
+ table.data = [];
+ getData(1);
+ function handleSelectionChange(val) {
+ table.selected = val;
+ // 导出订单/导出发货单
+ const exportLoading = ref(false);
+ async function onExport(type) {
+ exportLoading.value = true;
+ await api.order.export(type, { search: search.search });
+ exportLoading.value = false;
+ const batchHandleTools = [
+ type: 'all',
+ label: '批量发货',
+ auth: 'shop.admin.order.order.dispatch',
+ buttonType: 'default',
+ ];
+ function batchHandle() {
+ let order_ids = [];
+ table.selected.forEach((s) => {
+ order_ids.push(s.id);
+ OrderBatchDispatch,
+ title: '批量发货',
+ order_ids,
+ function detailRow(id) {
+ OrderDetail,
+ title: '订单详情',
+ type: 'detail',
+ id,
+ close: () => {
+ function onOpenAction(id) {
+ useModal(OrderAction, {
+ title: '日志',
+ const arraySpanMethod = ({ row, column, rowIndex, columnIndex }) => {
+ if (columnIndex == 2) {
+ return [1, 4];
+ } else if (columnIndex == 3 || columnIndex == 4 || columnIndex == 5) {
+ return [0, 0];
+ const objectSpanMethod = ({ row, column, rowIndex, columnIndex }) => {
+ if (columnIndex == 0) {
+ if (columnIndex == 1) {
+ return [1, 2];
+ if (columnIndex == 3 || columnIndex == 4 || columnIndex == 7) {
+ if (rowIndex == 0) {
+ return {
+ rowspan: 200,
+ colspan: 1,
+ rowspan: 0,
+ colspan: 0,
+ function onOpenGoodsEdit(id) {
+ useModal(GoodsEdit, {
+ title: '商品',
+ id: id,
+ function onSend(row) {
+ function onChangeFee(row) {
+ id: row.id,
+ pay_fee: row.pay_fee,
+ function onOpenRefund(row, index) {
+ refundPopover[index] = false;
+ money: row.pay_fee,
+ const refundPopover = reactive({});
+ async function onApplyRefundRefuse(id, index) {
+ const { error } = await api.order.applyRefundRefuse(id);
+ function onOpenZkhSubmitConfirm(order_item_id) {
+ ElMessageBox.confirm('确定要下单到震坤行吗?', '提示', {
+ confirmButtonText: '确定',
+ cancelButtonText: '取消',
+ type: 'warning',
+ .then(async () => {
+ const { error } = await api.order.zkhOrderSubmit({ order_item_id });
+ ElMessage.success('下单成功');
+ .catch(() => {
+ // 用户取消操作,不做任何处理
+ const confirmPopover = reactive({});
+ async function onOfflineRefuse(id, index) {
+ confirmPopover[index] = false;
+ const { error } = await api.order.offlineRefuse(id);
+ async function onOfflineConfirm(id, index) {
+ const { error } = await api.order.offlineConfirm(id);
+ function onOpenManualOrder(id) {
+ ManualOrder,
+ title: '手动下单',
+ onMounted(async () => {
+ await getType();
+ .order-page {
+ .el-main {
+ .el-table {
+ --el-table-row-hover-bg-color: var(--sa-background-hex-hover);
+ .el-table__row {
+ background: var(--sa-background-hex-hover);
+ .el-table__header-wrapper {
+ .el-table__expanded-cell {
+ padding: 0;
+ background: var(--sa-background-assist);
+ .sa-expand-table {
+ --el-table-row-hover-bg-color: var(--el-fill-color-light);
+ background: var(--el-table-tr-bg-color);
+ .el-table__body tr:hover > td.el-table__cell {
+ background-color: var(--sa-background-assist) !important;
+ .el-table__body tr.hover-row.current-row > td.el-table__cell,
+ .el-table__body tr.hover-row.el-table__row--striped.current-row > td.el-table__cell,
+ .el-table__body tr.hover-row.el-table__row--striped > td.el-table__cell,
+ .el-table__body tr.hover-row > td.el-table__cell {
+ .sa-table {
+ .apply-refund {
+ .order-wrap {
+ position: relative;
+ & > div {
+ margin-right: 24px;
+ .id {
+ min-width: 80px;
+ .order-sn {
+ min-width: 228px;
+ height: 14px;
+ width: 12px !important;
+ height: 12px !important;
+ .create-time {
+ color: var(--sa-subtfont);
+ .platform-text {
+ min-width: 116px;
+ .pay-types-text {
+ margin-right: 0;
+ .address-content {
+ width: 100%;
+ display: inline-flex;
+ .consignee {
+ text-align: left;
+ .address-name {
+ .address {
+ display: none;
+ .refund-popover,
+ .confirm-popover {
+ line-height: 20px;
+ margin-bottom: 16px;
+ .el-icon {
+ margin-right: 8px;
+ .tip {
+ .pay-fee-reference {
+ width: fit-content;
+ border-bottom: 1px dashed var(--t-color-primary);
+ .pay-fee-popover {
+ .pay-fee-item {
+ &.pay-fee-item-discount {
+ flex: 1;
+ .goods-id {
+ .goods-sku-text {
+ margin-bottom: 10px;
+ .goods-price {
+ .goods-num {
+ .activity-type-text {
+ height: 20px;
+ padding: 0 5px;
+ border-radius: 2px;
+ text-align: center;
+ &.groupon,
+ &.groupon_ladder {
+ background: var(--t-bg-active);
+ &.seckill {
+ background: rgba(255, 77, 79, 0.16);
+ .discount-fee {
+ text-decoration-line: underline;
+ .discount-fee-popover {
+ .promo-type-text {
+ padding: 0 8px;
+ background: rgba(250, 173, 20, 0.16);
+ border-radius: 10px;
+ justify-content: center;
+ .apply-refund-wrap {
+ .apply-refund-button {
+ height: 26px;
+ border-radius: 13px;
+ border: none;
+ padding: 0 8px 0 12px;
+ --el-button-bg-color: rgba(255, 77, 79, 0.16);
+ .send-button {
+ --el-button-bg-color: var(--t-bg-active);
@@ -6,10 +6,17 @@
<el-input v-model="form.model.name" placeholder="请填写设置名称"></el-input>
<el-form-item label="设置键" prop="key">
- <el-input v-model="form.model.key" placeholder="请填写设置键,如:order_timeout"></el-input>
+ v-model="form.model.key"
+ placeholder="请填写设置键,如:order_timeout"
<el-form-item label="数据类型" prop="type">
- <el-select v-model="form.model.type" placeholder="请选择数据类型" @change="handleTypeChange">
+ v-model="form.model.type"
+ placeholder="请选择数据类型"
+ @change="handleTypeChange"
<el-option label="字符串" value="string"></el-option>
<el-option label="数字" value="number"></el-option>
<el-option label="布尔值" value="boolean"></el-option>
@@ -17,37 +24,37 @@
<el-form-item label="设置值" prop="value">
v-if="form.model.type === 'string'"
- v-model="form.model.value"
+ v-model="form.model.value"
placeholder="请填写设置值"
v-else-if="form.model.type === 'number'"
placeholder="请填写数字"
style="width: 100%"
v-else-if="form.model.type === 'boolean'"
placeholder="请选择布尔值"
<el-option label="是" value="true"></el-option>
<el-option label="否" value="false"></el-option>
v-else-if="form.model.type === 'json'"
type="textarea"
placeholder="请填写JSON格式数据"
<el-form-item label="描述" prop="description">
placeholder="请填写设置描述"
@@ -72,7 +79,7 @@
- import { api } from './setting.service';
@@ -101,7 +108,7 @@
// 处理类型变化
function handleTypeChange(type) {
// 重置值
@@ -113,7 +120,7 @@
form.model.value = '';
@@ -109,7 +109,7 @@
- path: 'setting',
- name: 'shop.admin.order.setting',
- component: () => import('@/app/shop/admin/order/setting/index.vue'),
- title: '订单设置',
- ...CRUD('shop/admin/order_setting'),
- select: (params) => SELECT('shop/admin/order_setting', params),
@@ -1,74 +0,0 @@
- <el-table class="sa-table" :data="log.data" height="520" v-loading="log.loading">
- <el-table-column label="优惠券名称" min-width="160" align="center">
- <div class="sa-line-1">{{ scope.row.coupon?.name }}</div>
- <el-table-column label="优惠券面额" min-width="120" align="center">
- <div class="sa-line-1">{{ scope.row.coupon?.amount }}</div>
- <el-table-column prop="status_text" label="优惠券状态" min-width="120" align="center" />
- <el-table-column prop="create_time" label="领取时间" min-width="172" align="center" />
- <el-table-column prop="use_time" label="使用时间" min-width="172" align="center" />
- import { onMounted, reactive, watch } from 'vue';
- import { api } from './user.service.js';
- const emit = defineEmits(['updateRefresh']);
- const props = defineProps(['id', 'refresh']);
- const log = reactive({
- loading: false,
- log.loading = true;
- const { error, data } = await api.coupon(props.id, {
- log.data = data.data;
- log.loading = false;
- emit('updateRefresh', false);
- function onChangeLogTabName() {
- pageData.page = 1;
- pageData.list_rows = 10;
- pageData.total = 0;
- watch(
- () => props.refresh,
- () => {
- if (props.refresh) {
- onChangeLogTabName();
@@ -1,190 +0,0 @@
- <span class="left">领取记录</span>
- <search-condition :conditionLabel="filterParams.conditionLabel" @deleteFilter="deleteFilter"></search-condition>
- <el-table-column prop="id" label="ID" min-width="90" sortable="custom"> </el-table-column>
- <el-table-column label="用户信息" min-width="150">
- <sa-user-profile :user="scope.row.user" :id="scope.row.user_id"></sa-user-profile>
- <el-table-column prop="status_text" label="使用状态" min-width="120">
- <div :class="`sa-color--${statusClass[scope.row.status]}`">
- {{ scope.row.status_text || '-' }}
- <el-table-column label="有效期" min-width="320">
- {{ scope.row.use_start_time }}~{{ scope.row.use_end_time }}
- <el-table-column prop="use_time" label="使用时间" min-width="172">
- {{ scope.row.use_time || '-' }}
- <el-table-column prop="order_sn" label="订单号" min-width="280">
- {{ scope.row.order?.order_sn || '-' }}
- <el-table-column prop="create_time" label="领取时间" min-width="172"></el-table-column>
-import { api } from './user.service.js';
-import { usePagination } from '@/sheep/hooks';
-import { useSearch } from '@/sheep/components/sa-table/sa-search/useSearch';
-import { composeFilter } from '@/sheep/utils';
-import { cloneDeep } from 'lodash';
-import { useRoute } from 'vue-router';
-const route = useRoute();
-const filterParams = reactive({
- field: 'user',
- label: '用户信息',
- value: {
- field: 'user_id',
- options: [{
- label: '用户ID',
- value: 'user_id',
- label: '用户昵称',
- value: 'user.nickname',
- label: '用户手机号',
- value: 'user.mobile',
- }]
- 'order.order_sn': {
- field: 'order.order_sn',
- label: '订单号',
- placeholder: '请输入订单号',
- status: {
- field: 'status',
- label: '状态',
- data: [{
- label: '未使用',
- value: 'geted',
- label: '已使用',
- value: 'used',
- label: '已过期',
- value: 'expired',
- }],
- label: '领取时间',
- use_time: {
- field: 'use_time',
- label: '使用时间',
- user: { field: 'user_id', value: '' },
- 'order.order_sn': '',
- status: '',
- use_time: [],
-const { openFilter, deleteFilter } = useSearch({ filterParams, getData });
-const statusClass = {
- geted: 'info',
- used: 'success',
- expired: 'error',
-const table = reactive({
-const { pageData } = usePagination();
-// 获取数据
-async function getData(page) {
- 'user.nickname': 'like',
- 'user.mobile': 'like',
- use_time: 'range',
- const { data } = await api.couponList(route.query.couponid, {
@@ -9,27 +9,22 @@
<el-input v-model="form.model.icon" placeholder="请填写图标地址"></el-input>
<el-form-item label="升级条件" prop="upgrade_amount">
- v-model="form.model.upgrade_amount"
+ v-model="form.model.upgrade_amount"
placeholder="消费金额"
- <span style="margin-left: 10px;">元</span>
+ <span style="margin-left: 10px">元</span>
<el-form-item label="折扣率" prop="discount">
- v-model="form.model.discount"
- :min="1"
- :max="100"
- placeholder="折扣率"
+ <el-input-number v-model="form.model.discount" :min="1" :max="100" placeholder="折扣率" />
placeholder="请填写等级描述"
@@ -48,7 +43,7 @@
- import { api } from './level.service';
+ import { api } from '../user.service';
@@ -77,7 +72,7 @@
+ const { error, data } = await api.level.detail(id);
@@ -88,8 +83,8 @@
+ ? await api.level.add(submitForm)
+ : await api.level.edit(props.modal.params.id, submitForm);
<el-table-column label="等级图标" min-width="100">
v-if="scope.row.icon"
- :src="scope.row.icon"
+ :src="scope.row.icon"
style="width: 40px; height: 40px"
@@ -57,15 +57,11 @@
<el-table-column label="升级条件" min-width="150">
- 消费满 {{ scope.row.upgrade_amount || 0 }} 元
+ <span class="sa-table-line-1"> 消费满 {{ scope.row.upgrade_amount || 0 }} 元 </span>
<el-table-column label="折扣率" min-width="100">
- {{ scope.row.discount || 100 }}%
+ <template #default="scope"> {{ scope.row.discount || 100 }}% </template>
<el-table-column label="排序" min-width="100">
@@ -112,7 +108,7 @@
@@ -144,7 +140,7 @@
+ const { error, data } = await api.level.list({
@@ -207,7 +203,7 @@
+ await api.level.delete(id);
@@ -226,7 +222,7 @@
+ await api.level.edit(ids.join(','), {
- path: 'level',
- name: 'shop.admin.user.level',
- component: () => import('@/app/shop/admin/user/level/index.vue'),
- title: '会员等级',
- ...CRUD('shop/admin/user_level'),
- select: (params) => SELECT('shop/admin/user_level', params),
@@ -31,7 +31,7 @@
- import { api } from './list.service';
@@ -60,7 +60,7 @@
+ const { error, data } = await api.list.detail(id);
@@ -71,8 +71,8 @@
+ ? await api.list.add(submitForm)
+ : await api.list.edit(props.modal.params.id, submitForm);
@@ -147,7 +147,7 @@
+ const { error, data } = await api.list.list({
@@ -210,7 +210,7 @@
+ await api.list.delete(id);
@@ -229,7 +229,7 @@
+ await api.list.edit(ids.join(','), {
- path: 'list',
- name: 'shop.admin.user.list',
- component: () => import('@/app/shop/admin/user/list/index.vue'),
- title: '用户列表',
- ...CRUD('shop/admin/user'),
- select: (params) => SELECT('shop/admin/user', params),
@@ -1,70 +0,0 @@
- <el-table-column prop="create_time" label="下单时间" min-width="172" align="center" />
- <el-table-column prop="order_sn" label="订单号" min-width="280" align="center" />
- <el-table-column prop="platform_text" label="订单来源" min-width="120" align="center" />
- <el-table-column prop="type_text" label="订单类型" min-width="100" align="center" />
- <el-table-column prop="order_amount" label="订单总金额" min-width="140" align="center" />
- <el-table-column prop="total_discount_fee" label="优惠减免" min-width="140" align="center" />
- <el-table-column prop="pay_fee" label="实付金额" min-width="140" align="center" />
- <el-table-column prop="status_text" label="订单状态" min-width="100" align="center" />
- import { api } from '@/app/shop/admin/order/order.service.js';
- const { error, data } = await api.order.list({
- search: JSON.stringify({ user_id: [props.id] }),
- log.data = data.orders.data;
- pageData.page = data.orders.current_page;
- pageData.list_rows = data.orders.per_page;
- pageData.total = data.orders.total;
@@ -1,78 +0,0 @@
- <el-table-column prop="create_time" label="分享时间" min-width="172" align="center" />
- <el-table-column label="被分享用户" min-width="180">
- <sa-user-profile :user="scope.row.user" :id="scope.user_id" />
- <!-- TODO: 分享类型 -->
- <el-table-column prop="from_text" label="分享类型" min-width="120" align="center" />
- <el-table-column label="分享信息" min-width="180" show-overflow-tooltip>
- <sa-image :url="scope.row.ext?.image" size="32" />
- <div class="sa-line-1 sa-m-l-8">{{ scope.row.ext?.memo }}</div>
- <el-table-column prop="platform_text" label="平台" min-width="120" align="center" />
- const { error, data } = await api.share(props.id, {
@@ -6,12 +6,15 @@
<el-input v-model="form.model.name" placeholder="请填写标签名称"></el-input>
<el-form-item label="标签颜色" prop="color">
- <el-color-picker v-model="form.model.color" placeholder="请选择标签颜色"></el-color-picker>
+ <el-color-picker
+ v-model="form.model.color"
+ placeholder="请选择标签颜色"
+ ></el-color-picker>
placeholder="请填写标签描述"
@@ -30,7 +33,7 @@
- import { api } from './tag.service';
@@ -56,7 +59,7 @@
+ const { error, data } = await api.tag.detail(id);
@@ -67,8 +70,8 @@
+ ? await api.tag.add(submitForm)
+ : await api.tag.edit(props.modal.params.id, submitForm);
@@ -135,7 +135,7 @@
+ const { error, data } = await api.tag.list({
@@ -198,7 +198,7 @@
+ await api.tag.delete(id);
@@ -217,7 +217,7 @@
+ await api.tag.edit(ids.join(','), {
- path: 'tag',
- name: 'shop.admin.user.tag',
- component: () => import('@/app/shop/admin/user/tag/index.vue'),
- title: '标签管理',
- ...CRUD('shop/admin/user_tag'),
- select: (params) => SELECT('shop/admin/user_tag', params),
@@ -1,26 +1,70 @@
+import { CRUD } from '@/sheep/request/crud';
path: 'user',
name: 'shop.admin.user',
component: Content,
+ title: '用户',
- path: 'couponList',
- name: 'shop.admin.user.couponList',
- component: () => import('@/app/shop/admin/user/couponList.vue'),
+ name: 'shop.admin.user.list',
+ component: () => import('./list/index.vue'),
- title: '优惠券领取记录',
+ title: '用户列表',
+ path: 'level',
+ name: 'shop.admin.user.level',
+ component: () => import('./level/index.vue'),
+ title: '会员等级',
+ path: 'tag',
+ name: 'shop.admin.user.tag',
+ component: () => import('./tag/index.vue'),
+ title: '会员标签',
const api = {
+ // 用户列表相关 API
+ list: {
+ ...CRUD('shop/admin/user/list'),
+ export: (params) =>
+ url: 'shop/admin/user/list/export',
+ statistics: () =>
+ url: 'shop/admin/user/list/statistics',
+ // 会员等级相关 API
+ level: {
+ ...CRUD('shop/admin/user/level'),
+ // 会员标签相关 API
+ tag: {
+ ...CRUD('shop/admin/user/tag'),
+ // 其他用户相关 API
share: (id, params) =>
request({
url: `shop/admin/share/${id}`,
@@ -37,7 +81,7 @@ const api = {
url: `shop/admin/user/coupon/couponList/${id}`,
- params
@@ -1,75 +0,0 @@
- <el-table-column prop="create_time" label="交易时间" min-width="172" align="center" />
- <el-table-column prop="amount" label="变动余额" min-width="120" align="center" />
- <el-table-column prop="before" label="变更前" min-width="120" align="center" />
- <el-table-column prop="after" label="剩余余额" min-width="120" align="center" />
- <el-table-column label="操作人" min-width="160">
- <sa-user-profile type="oper" :user="scope.row.oper" :id="scope.row.oper_id" />
- <el-table-column label="备注" min-width="160" align="center">
- {{ scope.row.event_text }}{{ scope.row.memo ? ':' + scope.row.memo : '' }}
- import userApi from '@/app/user/api';
- const { error, data } = await userApi.walletLog.commission(props.id, {
- const { error, data } = await userApi.walletLog.money(props.id, {
- <el-table-column prop="amount" label="变动积分" min-width="120" align="center" />
- <el-table-column prop="after" label="剩余积分" min-width="120" align="center" />
- const { error, data } = await userApi.walletLog.score(props.id, {
@@ -1,443 +0,0 @@
- <el-container class="admin-detail">
- <el-main class="sa-p-b-0">
- <el-row :gutter="24">
- <el-col class="sa-col-24" :xs="24" :sm="24" :md="6" :lg="6" :xl="6">
- <div class="title">基本信息</div>
- <div class="bottom">
- <div class="sa-flex sa-row-between sa-m-b-16">
- <div class="sa-flex sa-flex-1">
- <sa-image :url="detail.data.avatar" size="64" radius="32" />
- <div class="sa-m-l-20 sa-flex-1">
- <div class="nickname sa-m-b-8 sa-table-line-1">
- {{ detail.data.nickname }}
- <div class="id">#{{ detail.data.id }}</div>
- <el-button v-auth="'user.admin.user.edit'" type="primary" @click="onUpload"
- >更换头像</el-button
- <div class="info">
- <el-col class="item sa-col-12" :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
- <div class="label">昵称</div>
- <div class="content">
- <el-input v-model="detail.data.nickname" />
- <div class="label">性别</div>
- <el-radio-group v-model="detail.data.gender">
- <el-radio :label="0">未知</el-radio>
- <el-radio :label="1">男</el-radio>
- <el-radio :label="2">女</el-radio>
- <div class="label">用户名</div>
- <el-input v-model="detail.data.username" placeholder="首位需为字母且不少五位">
- <template #suffix>
- <span v-html="settingStatus('username')"></span>
- <div class="label">密码</div>
- <el-input v-model="detail.data.password" placeholder="不修改则留空">
- <span v-html="settingStatus('password')"></span>
- <div class="label">手机号</div>
- <el-input v-model="detail.data.mobile" type="number">
- <span v-html="settingStatus('mobile')"></span>
- <div class="label">电子邮箱</div>
- <el-input v-model="detail.data.email">
- <span v-html="settingStatus('email')"></span>
- <div class="label">状态</div>
- <el-radio-group v-model="detail.data.status">
- <el-radio label="normal">正常</el-radio>
- <el-radio label="disabled">禁用</el-radio>
- <div class="label"></div>
- <div class="content sa-flex sa-row-right">
- <el-button v-auth="'user.admin.user.detail'" @click="getDetail">重置</el-button>
- v-throttle
- v-auth="'user.admin.user.edit'"
- @click="onSave"
- <el-col class="sa-col-24" :xs="24" :sm="24" :md="18" :lg="18" :xl="18">
- <el-col class="sa-col-24" :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
- <div class="title">账户信息</div>
- <div class="label">余额</div>
- <div class="content sa-flex">
- {{ detail.data.money }}
- v-auth="'user.admin.user.recharge'"
- class="is-link sa-m-l-12"
- @click="onRecharge('money')"
- >充值</el-button
- <div class="label">积分</div>
- {{ detail.data.score }}
- @click="onRecharge('score')"
- <div class="label">总计消费</div>
- {{ detail.data.total_consume }}
- <div class="label">上次登陆时间</div>
- <div class="content">{{ detail.data.login_time }}</div>
- <div class="label">登录IP</div>
- <div class="content">{{ detail.data.login_ip }}</div>
- <div class="label">注册时间</div>
- <div class="content">{{ detail.data.create_time }}</div>
- <el-col class="third-oauth sa-col-24" :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
- <div class="title">第三方账号</div>
- <el-row class="info">
- <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
- <div class="item" v-for="item in detail.data.third_oauth" :key="item.id">
- <div class="content sa-flex sa-row-between">
- :src="`./static/images/platform/${item.provider}/${item.platform}.png`"
- <span class="sa-m-l-16">
- {{ platform[item.provider][item.platform] }}
- <el-popover popper-class="sa-popper" placement="top-start" trigger="hover">
- <div class="label">登录次数:</div>
- <div class="content">{{ item.login_num }}</div>
- <div class="label">Openid:</div>
- <div class="content">{{ item.openid }}</div>
- <div v-if="item.unionid" class="item sa-flex">
- <div class="label">Unionid:</div>
- <div class="content">{{ item.unionid }}</div>
- <div class="label">更新时间:</div>
- <div class="content">{{ item.update_time }}</div>
- <sa-image :url="item.avatar" size="32" />
- <span class="name sa-m-l-12">
- {{ item.nickname }}
- <el-col class="log sa-col-24" :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
- <div class="title sa-flex"> 用户动态 </div>
- <el-tabs v-model="log.tabName">
- <el-tab-pane label="余额记录" name="money"></el-tab-pane>
- <el-tab-pane label="积分记录" name="score"></el-tab-pane>
- <el-tab-pane label="订单记录" name="order"></el-tab-pane>
- <el-tab-pane label="分享记录" name="share"></el-tab-pane>
- <el-tab-pane label="优惠券明细" name="coupon"></el-tab-pane>
- <component
- :is="`${log.tabName}-log`"
- :id="modal?.params?.id"
- :refresh="log.refresh"
- @updateRefresh="(val) => (log.refresh = val)"
- import MoneyLog from './components/money.vue';
- import ScoreLog from './components/score.vue';
- import OrderLog from '@/app/shop/admin/user/order.vue';
- import ShareLog from '@/app/shop/admin/user/share.vue';
- import CouponLog from '@/app/shop/admin/user/coupon.vue';
- components: {
- MoneyLog,
- ScoreLog,
- OrderLog,
- ShareLog,
- CouponLog,
- import { useFile, useModal } from '@/sheep/hooks';
- import AdminRecharge from './recharge.vue';
- const detail = reactive({
- detail.loading = true;
- const { error, data } = await userApi.detail(props.modal.params.id);
- error === 0 && (detail.data = data);
- detail.loading = false;
- function settingStatus(key) {
- let flag = detail.data.verification?.[key];
- return `<span style="color:${flag ? 'var(--el-color-success)' : 'var(--el-color-warning)'}">
- ${flag ? '已' : '未'}${key == 'username' || key == 'password' ? '设置' : '认证'}
- </span>`;
- const platform = {
- wechat: {
- openPlatform: '微信开放平台',
- miniProgram: '微信小程序',
- officialAccount: '微信公众平台',
- tabName: 'money',
- refresh: false,
- function onUpload() {
- useFile(
- fileType: 'image',
- confirm: (data) => {
- detail.data.avatar = data.url;
- async function onSave() {
- const { error } = await userApi.edit(props.modal.params.id, detail.data);
- error == 0 && getDetail();
- function onRecharge(type) {
- AdminRecharge,
- id: props.modal.params.id,
- type,
- log.refresh = true;
- .admin-detail {
- .bottom {
- .nickname {
- .id {
- padding: 16px 16px 0;
- .label {
- height: 12px;
- line-height: 12px;
- .third-oauth {
- padding: 20px 16px 0;
- img {
- width: 32px;
- .none {
- color: #999;
- .log {
- .el-tabs__nav-wrap::after {
- height: 0;
- .sa-table {
- height: 300px;
- .oper-type {
- .system {
- @media only screen and (max-width: 1200px) {
- @for $i from 0 through 24 {
- .sa-col-#{$i} {
- max-width: calc(calc($i * 100%) / 24) !important;
- flex: 0 0 calc(calc($i * 100%) / 24) !important;
@@ -1,240 +0,0 @@
- <span class="left">会员管理</span>
- v-auth="'user.admin.user.list'"
- @click="getData()"
- <el-main class="sa-p-0" v-loading="table.loading">
- <el-table class="sa-table" height="100%" :data="table.data" stripe @sort-change="fieldFilter">
- <el-table-column sortable="custom" prop="id" label="ID" min-width="80" />
- <sa-user-profile :user="scope.row" :id="scope.row.id" />
- <el-table-column prop="username" label="用户名" min-width="120" align="center" />
- <el-table-column prop="mobile" label="手机号" min-width="120" align="center" />
- prop="commission"
- label="佣金"
- min-width="110"
- align="center"
- prop="total_consume"
- label="总消费"
- prop="score"
- label="积分"
- prop="money"
- label="余额"
- prop="create_time"
- label="注册时间"
- prop="login_time"
- label="上次登录"
- v-auth="'user.admin.user.detail'"
- @click="onDetail(scope.row.id)"
- >查看</el-button
- @confirm="onDelete(scope.row.id)"
- <el-button v-auth="'user.admin.user.delete'" class="is-link" type="danger">
- name: 'user.admin',
- import { usePagination, useModal } from '@/sheep/hooks';
- import UserDetail from './detail.vue';
- label: '用户名',
- value: 'username',
- value: 'nickname',
- value: 'mobile',
- label: '邮箱',
- value: 'email',
- label: '注册时间',
- login_time: {
- label: '上次登录',
- field: 'login_time',
- user: { field: 'id', value: '' },
- login_time: [],
- table.loading = true;
- username: 'like',
- nickname: 'like',
- mobile: 'like',
- email: 'like',
- login_time: 'range',
- const { error, data } = await userApi.list({
- table.loading = false;
- function onDetail(id) {
- useModal(UserDetail, {
- title: '会员详情',
- async function onDelete(id) {
- const { error } = await userApi.delete(id);
- error == 0 && getData();