Merge branch 'dev_vite' of http://18.167.251.121:10003/aidlab/aida_front into dev_vite

This commit is contained in:
李志鹏
2026-04-28 17:25:01 +08:00
19 changed files with 2993 additions and 2231 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -8,16 +8,11 @@
style="width: 250px" style="width: 250px"
class="range_picker" class="range_picker"
v-model:value="rangePickerValue" v-model:value="rangePickerValue"
:placeholder="[ :placeholder="[$t('HistoryPage.StartDate'), $t('HistoryPage.EndDate')]"
$t('HistoryPage.StartDate'),
$t('HistoryPage.EndDate'),
]"
valueFormat="YYYY-MM-DD" valueFormat="YYYY-MM-DD"
> >
<template #suffixIcon> <template #suffixIcon>
<span <span class="icon iconfont range_picker_icon icon-rili"></span>
class="icon iconfont range_picker_icon icon-rili"
></span>
</template> </template>
</a-range-picker> </a-range-picker>
</div> </div>
@@ -63,38 +58,50 @@
show-search show-search
></a-select> ></a-select>
</div> </div>
<div class="admin_state_item">
<span>Orgnization:</span>
<a-select
v-model:value="organizationId"
placeholder="Select the organization"
allow-clear
style="width: 250px"
@popupScroll="handleOrganizationScroll"
>
<a-select-option
v-for="item in organizationOptions"
:key="item.id"
:value="item.id"
>
{{ item.name }}
</a-select-option>
</a-select>
</div>
</div> </div>
<div class="admin_search"> <div class="admin_search">
<div class="admin_search_item" @click="searchHistoryList" :style="{height:isAwayOrUnfold?'4rem':''}"> <div
class="admin_search_item"
@click="searchHistoryList"
:style="{ height: isAwayOrUnfold ? '4rem' : '' }"
>
Search Search
</div> </div>
<div class="admin_search_item" @click="addhHistoryList"> <div class="admin_search_item" @click="addhHistoryList">Add</div>
Add
</div>
</div> </div>
<div class="admin_state_list"> <div class="admin_state_list">
<div <div class="admin_state_list_item" @click="lastGeTrialList('year')">
class="admin_state_list_item"
@click="lastGeTrialList('year')"
>
Nearly a year Nearly a year
</div> </div>
<div <div class="admin_state_list_item" @click="lastGeTrialList('month')">
class="admin_state_list_item"
@click="lastGeTrialList('month')"
>
Last month Last month
</div> </div>
<div <div class="admin_state_list_item" @click="lastGeTrialList('week')">Last week</div>
class="admin_state_list_item"
@click="lastGeTrialList('week')"
>
Last week
</div>
</div> </div>
</div> </div>
<div class="awayOrUnfold" :class="{ active: isAwayOrUnfold }"> <div class="awayOrUnfold" :class="{ active: isAwayOrUnfold }">
<span class="icon iconfont menu_icon icon-xiala" @click="()=>isAwayOrUnfold = !isAwayOrUnfold"></span> <span
class="icon iconfont menu_icon icon-xiala"
@click="() => (isAwayOrUnfold = !isAwayOrUnfold)"
></span>
</div> </div>
<div class="admin_table_content" ref="historyTable"> <div class="admin_table_content" ref="historyTable">
<a-table <a-table
@@ -104,24 +111,19 @@
:data-source="dataList" :data-source="dataList"
:scroll="{ y: historyTableHeight }" :scroll="{ y: historyTableHeight }"
@change="changePage" @change="changePage"
:showSorterTooltip='false' :showSorterTooltip="false"
:pagination="{ :pagination="{
showSizeChanger: true, showSizeChanger: true,
current: currentPage, current: currentPage,
pageSize: pageSize, pageSize: pageSize,
total: total, total: total,
showQuickJumper: true, showQuickJumper: true,
bordered: false, bordered: false
}" }"
> >
<template #bodyCell="{ column, text, record, index }"> <template #bodyCell="{ column, text, record, index }">
<div class="operate_list" v-if="column?.Operations"> <div class="operate_list" v-if="column?.Operations">
<div <div class="operate_item" @click="setAagree(record)">Edit</div>
class="operate_item"
@click="setAagree(record)"
>
Edit
</div>
<!-- <div <!-- <div
class="operate_item" class="operate_item"
@click="deleteGroup(record, index)" @click="deleteGroup(record, index)"
@@ -132,24 +134,19 @@
</template> </template>
</a-table> </a-table>
</div> </div>
<allUserPoerationsVue ref="allUserPoerationsVue" @searchHistoryList="searchHistoryList"></allUserPoerationsVue> <allUserPoerationsVue
ref="allUserPoerationsVue"
@searchHistoryList="searchHistoryList"
/>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { import { defineComponent, ref, createVNode, computed, reactive, toRefs, onMounted } from "vue"
defineComponent, import { formatTime } from "@/tool/util"
ref, import { useStore } from "vuex"
createVNode, import { Https } from "@/tool/https"
computed, import allUserPoerationsVue from "./allUserPoerations.vue"
reactive, import SelectUser from "@/component/common/SelectUser.vue"
toRefs,
onMounted,
} from "vue";
import { formatTime } from "@/tool/util";
import { useStore } from "vuex";
import { Https } from "@/tool/https";
import allUserPoerationsVue from "./allUserPoerations.vue";
import SelectUser from '@/component/common/SelectUser.vue'
export default defineComponent({ export default defineComponent({
components: { allUserPoerationsVue, SelectUser }, components: { allUserPoerationsVue, SelectUser },
setup() { setup() {
@@ -159,7 +156,7 @@ export default defineComponent({
tableLoading: false, tableLoading: false,
allCountry: [], allCountry: [],
isAwayOrUnfold: false isAwayOrUnfold: false
}); })
let filterData: any = reactive({ let filterData: any = reactive({
rangePickerValue: [], rangePickerValue: [],
currentPage: 1, currentPage: 1,
@@ -172,40 +169,41 @@ export default defineComponent({
occupation: "", occupation: "",
systemUser: "", systemUser: "",
order: "", //'Ascending 升序 Descending 降序' order: "", //'Ascending 升序 Descending 降序'
orderBy:'', orderBy: "",
userName: "", userName: "",
}); organizationId: null
})
let state: any = ref([ let state: any = ref([
{ {
label: "all", label: "all",
value: "", value: ""
}, },
{ {
label:'visitor', label: "visitor",
value:'0', value: "0"
}, },
{ {
label:'yearly', label: "yearly",
value:'1', value: "1"
}, },
{ {
label:'monthly', label: "monthly",
value:'2', value: "2"
}, },
{ {
label:'trial', label: "trial",
value:'3', value: "3"
}, },
{ {
label: "userInEvent", label: "userInEvent",
value: "4", value: "4"
}, },
{ {
label: "Edu Admin", label: "Edu Admin",
value: "7", value: "7"
}, }
]); ])
let renameData: any = ref({}); //修改名字选中的数据 let renameData: any = ref({}) //修改名字选中的数据
const columns: any = computed(() => { const columns: any = computed(() => {
return [ return [
{ {
@@ -215,7 +213,7 @@ export default defineComponent({
key: "id", key: "id",
width: 100, width: 100,
fixed: "left", fixed: "left",
sorter: true, sorter: true
}, },
{ {
title: "Email", title: "Email",
@@ -246,7 +244,7 @@ export default defineComponent({
dataIndex: "language", dataIndex: "language",
key: "language", key: "language",
width: 100, width: 100,
ellipsis:true, ellipsis: true
}, },
{ {
title: "Valid Start Time", title: "Valid Start Time",
@@ -256,12 +254,12 @@ export default defineComponent({
width: 200, width: 200,
ellipsis: true, ellipsis: true,
customRender: (record: any) => { customRender: (record: any) => {
let time = '' let time = ""
if (record.text) { if (record.text) {
time = formatTime(record.text / 1000, 'YYYY-MM-DD hh:mm:ss') time = formatTime(record.text / 1000, "YYYY-MM-DD hh:mm:ss")
} }
return time return time
}, }
}, },
{ {
title: "Valid End Time", title: "Valid End Time",
@@ -271,19 +269,19 @@ export default defineComponent({
width: 200, width: 200,
ellipsis: true, ellipsis: true,
customRender: (record: any) => { customRender: (record: any) => {
let time = '' let time = ""
if (record.text) { if (record.text) {
time = formatTime(record.text / 1000, 'YYYY-MM-DD hh:mm:ss') time = formatTime(record.text / 1000, "YYYY-MM-DD hh:mm:ss")
} }
return time return time
}, }
}, },
{ {
title: "Country or Region", title: "Country or Region",
align: "center", align: "center",
dataIndex: "country", dataIndex: "country",
key: "country", key: "country",
width:200, width: 200
}, },
{ {
title: "Create Date", title: "Create Date",
@@ -291,7 +289,7 @@ export default defineComponent({
dataIndex: "createDate", dataIndex: "createDate",
key: "createDate", key: "createDate",
width: 200, width: 200,
sorter: true, sorter: true
}, },
{ {
title: "Is Beginner", title: "Is Beginner",
@@ -301,21 +299,21 @@ export default defineComponent({
width: 80, width: 80,
ellipsis: true, ellipsis: true,
customRender: (record: any) => { customRender: (record: any) => {
let str; let str
if (record.value == 1) { if (record.value == 1) {
str = "Yes"; str = "Yes"
} else { } else {
str = "No"; str = "No"
}
return str
} }
return str;
},
}, },
{ {
title: 'Machine Room Ip', title: "Machine Room Ip",
align: "center", align: "center",
dataIndex: "browserIdentifiers", dataIndex: "browserIdentifiers",
key: "browserIdentifiers", key: "browserIdentifiers",
width:200, width: 200
}, },
{ {
title: "Credits", title: "Credits",
@@ -327,10 +325,10 @@ export default defineComponent({
dataIndex: "credits", dataIndex: "credits",
key: "credits", key: "credits",
width: 100, width: 100,
sorter: true, sorter: true
}, },
{ {
title: 'User Type', title: "User Type",
align: "center", align: "center",
// width: 150, // width: 150,
// minWidth: 100, // minWidth: 100,
@@ -340,22 +338,22 @@ export default defineComponent({
key: "systemUser", key: "systemUser",
width: 100, width: 100,
customRender: (record: any) => { customRender: (record: any) => {
let str; let str
if (record.value == 0) { if (record.value == 0) {
str = "visitor"; str = "visitor"
} else if (record.value == 1) { } else if (record.value == 1) {
str = "yearly"; str = "yearly"
} else if (record.value == 2) { } else if (record.value == 2) {
str = "monthly"; str = "monthly"
} else if (record.value == 3) { } else if (record.value == 3) {
str = "trial"; str = "trial"
} else if (record.value == 4) { } else if (record.value == 4) {
str = "userInEvent"; str = "userInEvent"
} else if (record.value == 7) { } else if (record.value == 7) {
str = "Edu Admin"; str = "Edu Admin"
}
return str
} }
return str;
},
}, },
{ {
title: "Operations", title: "Operations",
@@ -364,58 +362,58 @@ export default defineComponent({
align: "center", align: "center",
fixed: "right", fixed: "right",
// slots:{customRender:'action'} // slots:{customRender:'action'}
Operations: true, Operations: true
}, }
]; ]
}); })
//改变页码 //改变页码
let changePage = (e: any, filters: any, sorter: any) => { let changePage = (e: any, filters: any, sorter: any) => {
filterData.currentPage = e.current; filterData.currentPage = e.current
filterData.pageSize = e.pageSize; filterData.pageSize = e.pageSize
if (sorter.order) { if (sorter.order) {
if(sorter.columnKey == 'id'){ if (sorter.columnKey == "id") {
filterData.orderBy = 'id' filterData.orderBy = "id"
} else if (sorter.columnKey == "createDate") { } else if (sorter.columnKey == "createDate") {
filterData.orderBy = 'time' filterData.orderBy = "time"
} else if (sorter.columnKey == "credits") { } else if (sorter.columnKey == "credits") {
filterData.orderBy = 'credits' filterData.orderBy = "credits"
} }
} }
if (sorter.order) { if (sorter.order) {
filterData.order = sorter.order == "descend" ? "Descending" : "Ascending"; filterData.order = sorter.order == "descend" ? "Descending" : "Ascending"
} else { } else {
filterData.order = '' filterData.order = ""
}
gettrialList()
} }
gettrialList();
};
//查询列表 //查询列表
let searchHistoryList = () => { let searchHistoryList = () => {
filterData.currentPage = 1; filterData.currentPage = 1
gettrialList(); gettrialList()
}; }
let clearHistoryList = () => { let clearHistoryList = () => {
filterData.rangePickerValue = [], ;((filterData.rangePickerValue = []),
filterData.currentPage = 1, (filterData.currentPage = 1),
filterData.pageSize = 10, (filterData.pageSize = 10),
filterData.total = 0, (filterData.total = 0),
filterData.country = "", (filterData.country = ""),
filterData.email = "", (filterData.email = ""),
filterData.userType = "", (filterData.userType = ""),
filterData.ids = [], (filterData.ids = []),
filterData.occupation = "", (filterData.occupation = ""),
filterData.order = "", //'Ascending 升序 Descending 降序' (filterData.order = ""), //'Ascending 升序 Descending 降序'
filterData.orderBy = "", //'Ascending 升序 Descending 降序' (filterData.orderBy = ""), //'Ascending 升序 Descending 降序'
filterData.systemUser = "", (filterData.systemUser = ""),
filterData.userName = ""; (filterData.userName = ""))
}; }
let setHistoryListData = () => { let setHistoryListData = () => {
let startDate: any = filterData.rangePickerValue?.[0] let startDate: any = filterData.rangePickerValue?.[0]
? filterData.rangePickerValue[0] + " " + "00:00:00" ? filterData.rangePickerValue[0] + " " + "00:00:00"
: ""; : ""
let endDate: any = filterData.rangePickerValue?.[1] let endDate: any = filterData.rangePickerValue?.[1]
? filterData.rangePickerValue[1] + " " + "23:59:59" ? filterData.rangePickerValue[1] + " " + "23:59:59"
: ""; : ""
let data = { let data = {
endTime: endDate, endTime: endDate,
startTime: startDate, startTime: startDate,
@@ -430,63 +428,118 @@ export default defineComponent({
order: filterData.order, order: filterData.order,
orderBy: filterData.orderBy, orderBy: filterData.orderBy,
userName: filterData.userName, userName: filterData.userName,
}; organizationId: filterData.organizationId
return data; }
}; return data
}
//获取列表 //获取列表
let gettrialList = () => { let gettrialList = () => {
filter.tableLoading = true; filter.tableLoading = true
let data = setHistoryListData(); let data = setHistoryListData()
Https.axiosPost(Https.httpUrls.getUserInfo, data).then( Https.axiosPost(Https.httpUrls.getUserInfo, data).then((rv: any) => {
(rv: any) => {
if (rv) { if (rv) {
// this.dataList = rv // this.dataList = rv
filter.dataList = rv.records; filter.dataList = rv.records
filterData.total = rv.total; filterData.total = rv.total
filter.tableLoading = false; filter.tableLoading = false
// this.workspaceItem.position = this.singleTypeList[0].label // this.workspaceItem.position = this.singleTypeList[0].label
} }
})
} }
);
};
let lastGeTrialList = (str: string) => { let lastGeTrialList = (str: string) => {
clearHistoryList(); clearHistoryList()
let currentDate = new Date(); let currentDate = new Date()
let currentTimestamp = Math.floor(currentDate.getTime() / 1000); let currentTimestamp = Math.floor(currentDate.getTime() / 1000)
// 计算30天前的时间戳 // 计算30天前的时间戳
let thirtyDaysAgoTimestamp; let thirtyDaysAgoTimestamp
if (str == "year") { if (str == "year") {
thirtyDaysAgoTimestamp = currentTimestamp - 360 * 24 * 60 * 60; thirtyDaysAgoTimestamp = currentTimestamp - 360 * 24 * 60 * 60
} else if (str == "month") { } else if (str == "month") {
thirtyDaysAgoTimestamp = currentTimestamp - 30 * 24 * 60 * 60; thirtyDaysAgoTimestamp = currentTimestamp - 30 * 24 * 60 * 60
} else if (str == "week") { } else if (str == "week") {
thirtyDaysAgoTimestamp = currentTimestamp - 7 * 24 * 60 * 60; thirtyDaysAgoTimestamp = currentTimestamp - 7 * 24 * 60 * 60
}
filterData.rangePickerValue[0] = formatTime(thirtyDaysAgoTimestamp, "YYYY-MM-DD")
gettrialList()
} }
filterData.rangePickerValue[0] = formatTime(
thirtyDaysAgoTimestamp,
"YYYY-MM-DD"
);
gettrialList();
};
let filterOption = (input: any, option: any) => { let filterOption = (input: any, option: any) => {
// 使用 option.label 进行搜索 // 使用 option.label 进行搜索
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0; return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}; }
let addhHistoryList = () => { let addhHistoryList = () => {
allUserPoerationsVue.value.init('Add','') allUserPoerationsVue.value.init("Add", "")
}; }
let allUserPoerationsVue = ref() let allUserPoerationsVue = ref()
let setAagree = (data: any) => { let setAagree = (data: any) => {
allUserPoerationsVue.value.init('Edit',data) allUserPoerationsVue.value.init("Edit", data)
} }
const organizationOptions = ref([])
const organizationParams = reactive({
page: 1,
size: 10,
total: 0
})
const organizationLoading = ref(false)
const getOrganizationList = async (isLoadMore = false) => {
console.log("111111")
if (organizationLoading.value) return
if (isLoadMore) {
const loaded = organizationParams.page * organizationParams.size
if (organizationParams.total && loaded >= organizationParams.total) return
organizationParams.page += 1
} else {
organizationParams.page = 1
organizationOptions.value = []
}
organizationLoading.value = true
try {
const { total, ...requestParams } = organizationParams
const rv: any = await Https.axiosPost(
Https.httpUrls.queryOrganization,
requestParams
)
if (rv) {
const newRecords = rv.records || []
// 遍历新数据,如果已存在则覆盖,不存在则追加
newRecords.forEach((newOrg: any) => {
const newOrgId = String(newOrg.id)
const existingIndex = organizationOptions.value.findIndex(
(org: any) => String(org.id) === newOrgId
)
if (existingIndex !== -1) {
// 如果已存在,用新数据覆盖旧项
organizationOptions.value[existingIndex] = newOrg
} else {
// 如果不存在,追加到末尾
organizationOptions.value.push(newOrg)
}
})
organizationParams.total = rv.total || 0
}
} finally {
organizationLoading.value = false
}
}
const handleOrganizationScroll = (e: any) => {
const target = e?.target
if (!target) return
const nearBottom = target.scrollTop + target.clientHeight >= target.scrollHeight - 20
if (nearBottom) {
getOrganizationList(true)
}
}
onMounted(() => { onMounted(() => {
let allCountry: any = sessionStorage.getItem("allCountry"); let allCountry: any = sessionStorage.getItem("allCountry")
if (allCountry) { if (allCountry) {
filter.allCountry = JSON.parse(allCountry); filter.allCountry = JSON.parse(allCountry)
} }
gettrialList(); gettrialList()
}); getOrganizationList()
})
return { return {
...toRefs(filter), ...toRefs(filter),
...toRefs(filterData), ...toRefs(filterData),
@@ -501,22 +554,26 @@ export default defineComponent({
filterOption, filterOption,
allUserPoerationsVue, allUserPoerationsVue,
setAagree, setAagree,
}; handleOrganizationScroll,
getOrganizationList,
organizationOptions,
organizationParams
}
}, },
data() { data() {
return { return {
historyTableHeight: 0, historyTableHeight: 0,
handleResizeColumn: (w: any, col: any) => { handleResizeColumn: (w: any, col: any) => {
col.width = w; col.width = w
}, }
}; }
}, },
mounted() { mounted() {
let historyTable: any = this.$refs.historyTable; let historyTable: any = this.$refs.historyTable
this.historyTableHeight = historyTable.clientHeight - 200; this.historyTableHeight = historyTable.clientHeight - 200
}, },
methods: {}, methods: {}
}); })
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.admin_page .admin_table_search .admin_state { .admin_page .admin_table_search .admin_state {
@@ -524,6 +581,5 @@ export default defineComponent({
flex-wrap: wrap; flex-wrap: wrap;
} }
.admin_page { .admin_page {
} }
</style> </style>

View File

@@ -5,8 +5,8 @@
class="search-form" class="search-form"
layout="inline" layout="inline"
:model="searchForm" :model="searchForm"
:label-col="{ style: { width: '12rem' } }" :label-col="{ style: { width: '14rem' } }"
:wrapper-col="{ style: { width: '22rem' } }" :wrapper-col="{ style: { width: '20rem' } }"
> >
<a-form-item label="ID"> <a-form-item label="ID">
<a-input v-model:value="searchForm.id" allow-clear placeholder="Input the id" /> <a-input v-model:value="searchForm.id" allow-clear placeholder="Input the id" />
@@ -72,7 +72,7 @@
:options="countryList" :options="countryList"
/> />
</a-form-item> </a-form-item>
<a-form-item> <a-form-item class="search-form__actions">
<a-space> <a-space>
<a-button type="primary" @click="handleSearch">Search</a-button> <a-button type="primary" @click="handleSearch">Search</a-button>
<a-button @click="handleReset">Reset</a-button> <a-button @click="handleReset">Reset</a-button>
@@ -92,6 +92,7 @@
:loading="tableLoading" :loading="tableLoading"
:bordered="false" :bordered="false"
row-key="id" row-key="id"
:customRow="customPlanRow"
@change="changePage" @change="changePage"
@resizeColumn="handleResizeColumn" @resizeColumn="handleResizeColumn"
:scroll="{ y: historyTableHeight }" :scroll="{ y: historyTableHeight }"
@@ -107,10 +108,11 @@
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template <template
v-if=" v-if="
column.key === 'currentPeriodStart' || column.key === 'currentPeriodEnd' column.key === 'currentPeriodStart' ||
column.key === 'currentPeriodEnd'
" "
> >
{{ formatTime(record[column.key], 'YYYY-MM-DD hh:mm:ss') }} {{ formatTime(record[column.key], "YYYY-MM-DD hh:mm:ss") }}
</template> </template>
<template v-if="column.key === 'status'"> <template v-if="column.key === 'status'">
@@ -120,15 +122,15 @@
</template> </template>
<template v-else-if="column.key === 'actions'"> <template v-else-if="column.key === 'actions'">
<a-space> <a-space class="plan-row-actions" @click.stop>
<a @click="openEdit(record)">Edit</a> <a @click.stop="openEdit(record)">Edit</a>
<a-popconfirm <a-popconfirm
title="Confirm to delete this subscription plan?" title="Confirm to delete this subscription plan?"
ok-text="Confirm" ok-text="Confirm"
cancel-text="Cancel" cancel-text="Cancel"
@confirm="removePlan(record.id)" @confirm="removePlan(record.id)"
> >
<a class="danger-text">Delete</a> <a class="danger-text" @click.stop>Delete</a>
</a-popconfirm> </a-popconfirm>
</a-space> </a-space>
</template> </template>
@@ -137,6 +139,50 @@
</div> </div>
</a-card> </a-card>
<a-modal
v-model:visible="userInfoModalVisible"
title="User Info"
width="80%"
:footer="null"
:destroy-on-close="true"
class="user-info-modal"
>
<a-table
:columns="userInfoColumns"
:data-source="userInfoList"
:loading="userInfoLoading"
:pagination="{
showSizeChanger: true,
current: userInfoPage.current,
pageSize: userInfoPage.size,
total: userInfoPage.total,
showQuickJumper: true,
bordered: false
}"
row-key="id"
:scroll="{ x: 1280, y: 420 }"
@change="changeUserInfoPage"
>
<template #bodyCell="{ column, record }">
<template
v-if="
column.key === 'validStartTime' ||
column.key === 'validEndTime' ||
column.key === 'createDate'
"
>
{{ formatUserTime(record[column.key]) }}
</template>
<template v-else-if="column.key === 'systemUser'">
{{ getSystemUserLabel(record.systemUser) }}
</template>
<template v-else-if="column.key === 'isBeginner'">
{{ record.isBeginner == 1 ? "Yes" : "No" }}
</template>
</template>
</a-table>
</a-modal>
<div class="subscriptionPlanModal" ref="subscriptionPlanModal"></div> <div class="subscriptionPlanModal" ref="subscriptionPlanModal"></div>
<a-modal <a-modal
class="subscriptionPlan_modal generalModel" class="subscriptionPlan_modal generalModel"
@@ -213,7 +259,10 @@
@select="handleOrganizationSelect" @select="handleOrganizationSelect"
@change="handleOrganizationChange" @change="handleOrganizationChange"
> >
<a-select-option value="ADD_ORGANIZATION" class="add-organization-option"> <a-select-option
value="ADD_ORGANIZATION"
class="add-organization-option"
>
+ Create Organization + Create Organization
</a-select-option> </a-select-option>
<a-select-option <a-select-option
@@ -393,27 +442,30 @@ import {
ref, ref,
onMounted, onMounted,
onBeforeUnmount, onBeforeUnmount,
onActivated,
computed, computed,
nextTick, nextTick,
useTemplateRef useTemplateRef
} from 'vue' } from "vue"
import SelectUser from '@/component/common/SelectUser.vue' import SelectUser from "@/component/common/SelectUser.vue"
import { message } from 'ant-design-vue' import { message } from "ant-design-vue"
import { Https } from '@/tool/https' import { Https } from "@/tool/https"
import { formatTime } from '@/tool/util' import { formatTime } from "@/tool/util"
import store from '@/store' import store from "@/store"
import type { FormInstance, Rule } from 'ant-design-vue/es/form' import type { FormInstance, Rule } from "ant-design-vue/es/form"
import { debounce } from 'lodash-es' import { debounce } from "lodash-es"
import dayjs, { Dayjs } from 'dayjs' import dayjs, { Dayjs } from "dayjs"
type PlanStatus = 'PENDING' | 'ACTIVE' | 'EXPIRED' type PlanStatus = "PENDING" | "ACTIVE" | "EXPIRED"
interface SubscriptionPlan { interface SubscriptionPlan {
id: number id: number
userId?: string | number
name: string name: string
currentPeriodStart: string currentPeriodStart: string
currentPeriodEnd: string currentPeriodEnd: string
organizationId: string organizationId: string
adminAccId: string adminAccId: string
adminAccEmail?: string
status: PlanStatus status: PlanStatus
creditLimit: number creditLimit: number
accountNum?: number accountNum?: number
@@ -421,17 +473,32 @@ interface SubscriptionPlan {
endStamp: number endStamp: number
} }
interface UserInfoRecord {
id: number
userEmail?: string
userName?: string
language?: string
validStartTime?: number | string
validEndTime?: number | string
country?: string
createDate?: number | string
isBeginner?: number
browserIdentifiers?: string
credits?: number
systemUser?: number | string
}
const countryList = ref([]) const countryList = ref([])
const userRef = ref(null) const userRef = ref(null)
const searchForm = reactive({ const searchForm = reactive({
name: '', name: "",
startTime: '', startTime: "",
endTime: '', endTime: "",
organizationId: undefined as string | undefined, organizationId: undefined as string | undefined,
adminAccId: undefined as string | undefined, adminAccId: undefined as string | undefined,
status: [] as PlanStatus[] | [], status: [] as PlanStatus[] | [],
id: '', id: "",
countryOrRegion: null, countryOrRegion: null,
page: 1, page: 1,
size: 10, size: 10,
@@ -442,15 +509,24 @@ const toSeconds = (dateStr: string) => Math.floor(new Date(dateStr).getTime() /
const tableData = ref<SubscriptionPlan[]>([]) const tableData = ref<SubscriptionPlan[]>([])
const tableLoading = ref(false) const tableLoading = ref(false)
const userInfoModalVisible = ref(false)
const userInfoLoading = ref(false)
const userInfoList = ref<UserInfoRecord[]>([])
const currentUserInfoPlanId = ref<string | number | null>(null)
const userInfoPage = reactive({
current: 1,
size: 10,
total: 0
})
const modalVisible = ref(false) const modalVisible = ref(false)
const confirmLoading = ref(false) const confirmLoading = ref(false)
const modalTitle = ref('New Subscription Plan') const modalTitle = ref("New Subscription Plan")
const isEditMode = ref(false) const isEditMode = ref(false)
const formState = reactive({ const formState = reactive({
name: '', name: "",
currentPeriodStart: '', currentPeriodStart: "",
currentPeriodEnd: '', currentPeriodEnd: "",
organizationId: undefined as string | undefined, organizationId: undefined as string | undefined,
adminAccId: undefined as string | undefined, adminAccId: undefined as string | undefined,
creditLimit: null as number | null, creditLimit: null as number | null,
@@ -461,44 +537,44 @@ const formState = reactive({
const organizationModalVisible = ref(false) const organizationModalVisible = ref(false)
const organizationForm = reactive({ const organizationForm = reactive({
name: '', name: "",
type: undefined as string | undefined type: undefined as string | undefined
}) })
const statusLabelMap: Record<PlanStatus, string> = { const statusLabelMap: Record<PlanStatus, string> = {
PENDING: 'Pending', PENDING: "Pending",
ACTIVE: 'Active', ACTIVE: "Active",
EXPIRED: 'Expired' EXPIRED: "Expired"
} }
const statusColorMap: Record<PlanStatus, string> = { const statusColorMap: Record<PlanStatus, string> = {
PENDING: 'blue', PENDING: "blue",
ACTIVE: 'green', ACTIVE: "green",
EXPIRED: 'red' EXPIRED: "red"
} }
const statusOption = ref([ const statusOption = ref([
{ {
label: 'Pending', label: "Pending",
value: 'PENDING' value: "PENDING"
}, },
{ {
label: 'Active', label: "Active",
value: 'ACTIVE' value: "ACTIVE"
}, },
{ {
label: 'Expired', label: "Expired",
value: 'EXPIRED' value: "EXPIRED"
} }
]) ])
const disabledDate = (current: Dayjs) => { const disabledDate = (current: Dayjs) => {
return current && current < dayjs().subtract(1, 'days').endOf('day') return current && current < dayjs().subtract(1, "days").endOf("day")
} }
const disableEndDate = (current: Dayjs) => { const disableEndDate = (current: Dayjs) => {
if (isEditMode.value) { if (isEditMode.value) {
const specificTime = dayjs(formState.currentPeriodEnd) const specificTime = dayjs(formState.currentPeriodEnd)
return current && current < dayjs(formState.currentPeriodEnd * 1000).startOf('day') return current && current < dayjs(formState.currentPeriodEnd * 1000).startOf("day")
} }
return disabledDate(current) return disabledDate(current)
} }
@@ -509,7 +585,7 @@ const range = (start: number, end: number) => {
} }
return result return result
} }
const disableEndTime = date => { const disableEndTime = (date) => {
if (!formState.currentPeriodEnd || !isEditMode.value) if (!formState.currentPeriodEnd || !isEditMode.value)
return { return {
disabledHours: () => [], disabledHours: () => [],
@@ -519,7 +595,7 @@ const disableEndTime = date => {
const specificTime = dayjs.unix(formState.currentPeriodEnd) const specificTime = dayjs.unix(formState.currentPeriodEnd)
if (date && date.isSame(specificTime, 'day')) { if (date && date.isSame(specificTime, "day")) {
// 如果是指定日期当天,禁用时间戳之前的时间 // 如果是指定日期当天,禁用时间戳之前的时间
const hour = specificTime.hour() const hour = specificTime.hour()
const minute = specificTime.minute() const minute = specificTime.minute()
@@ -527,7 +603,7 @@ const disableEndTime = date => {
return { return {
disabledHours: () => Array.from({ length: hour }, (_, i) => i), // 禁用小时之前 disabledHours: () => Array.from({ length: hour }, (_, i) => i), // 禁用小时之前
disabledMinutes: selectedHour => { disabledMinutes: (selectedHour) => {
if (selectedHour === hour) { if (selectedHour === hour) {
return Array.from({ length: minute }, (_, i) => i) // 同小时,禁用分钟之前 return Array.from({ length: minute }, (_, i) => i) // 同小时,禁用分钟之前
} }
@@ -555,114 +631,328 @@ const normalizeStatus = (status?: string): PlanStatus | undefined => {
return upper return upper
} }
const getStatusColor = (status?: string) => const getStatusColor = (status?: string) =>
statusColorMap[normalizeStatus(status) as PlanStatus] || 'default' statusColorMap[normalizeStatus(status) as PlanStatus] || "default"
const columns = [ const columns = [
{ title: 'Name', dataIndex: 'name', key: 'name', align: 'center', width: 180 }, { title: "Name", dataIndex: "name", key: "name", align: "center", width: 180 },
{ title: 'ID', dataIndex: 'id', key: 'id', align: 'center', width: 80 }, { title: "ID", dataIndex: "id", key: "id", align: "center", width: 80 },
{ {
title: 'Organization', title: "Organization",
dataIndex: 'organizationName', dataIndex: "organizationName",
key: 'organizationName', key: "organizationName",
align: 'center', align: "center",
width: 180 width: 180
}, },
{ {
title: 'Admin Account', title: "Admin Account",
dataIndex: 'adminAccEmail', dataIndex: "adminAccEmail",
key: 'adminAccEmail', key: "adminAccEmail",
align: 'center', align: "center",
width: 180, width: 180,
ellipsis: true ellipsis: true
}, },
{ {
title: 'Sub-Account Num', title: "Sub-Account Num",
dataIndex: 'accountNum', dataIndex: "accountNum",
key: 'accountNum', key: "accountNum",
align: 'center', align: "center",
width: 120, width: 120,
ellipsis: true ellipsis: true
}, },
{ {
title: 'Country or Region', title: "Country or Region",
dataIndex: 'countryOrRegion', dataIndex: "countryOrRegion",
key: 'countryOrRegion', key: "countryOrRegion",
align: 'center', align: "center",
width: 120, width: 120,
ellipsis: true ellipsis: true
}, },
{ {
title: 'Start Time', title: "Start Time",
dataIndex: 'currentPeriodStart', dataIndex: "currentPeriodStart",
key: 'currentPeriodStart', key: "currentPeriodStart",
align: 'center', align: "center",
width: 200 width: 200
}, },
{ {
title: 'End Time', title: "End Time",
dataIndex: 'currentPeriodEnd', dataIndex: "currentPeriodEnd",
key: 'currentPeriodEnd', key: "currentPeriodEnd",
align: 'center', align: "center",
width: 200 width: 200
}, },
{ title: 'Status', dataIndex: 'status', key: 'status', align: 'center', width: 100 }, { title: "Status", dataIndex: "status", key: "status", align: "center", width: 100 },
{ {
title: 'Credit Limit', title: "Credit Limit",
dataIndex: 'creditLimit', dataIndex: "creditLimit",
key: 'creditLimit', key: "creditLimit",
align: 'center', align: "center",
width: 120 width: 120
}, },
{ title: 'Operations', key: 'actions', width: 160, align: 'center', fixed: 'right' } { title: "Operations", key: "actions", width: 160, align: "center", fixed: "right" }
] ]
const historyTable = ref() const userInfoColumns = [
{ title: "User Id", dataIndex: "id", key: "id", align: "center", width: 100 },
{
title: "Email",
dataIndex: "userEmail",
key: "userEmail",
align: "center",
width: 200,
ellipsis: true
},
{
title: "User Name",
dataIndex: "userName",
key: "userName",
align: "center",
width: 150,
ellipsis: true
},
{
title: "Valid Start Time",
dataIndex: "validStartTime",
key: "validStartTime",
align: "center",
width: 200
},
{
title: "Valid End Time",
dataIndex: "validEndTime",
key: "validEndTime",
align: "center",
width: 200
},
{
title: "Country or Region",
dataIndex: "country",
key: "country",
align: "center",
width: 180,
ellipsis: true
},
{
title: "Credits",
dataIndex: "credits",
key: "credits",
align: "center",
width: 100
},
{
title: "User Type",
dataIndex: "systemUser",
key: "systemUser",
align: "center",
width: 120
}
]
const systemUserLabelMap: Record<string, string> = {
"0": "visitor",
"1": "yearly",
"2": "monthly",
"3": "trial",
"4": "userInEvent",
"7": "Edu Admin"
}
const getSystemUserLabel = (systemUser?: number | string) => {
if (systemUser === undefined || systemUser === null) return ""
return systemUserLabelMap[String(systemUser)] || String(systemUser)
}
const formatUserTime = (value?: number | string) => {
if (!value) return ""
if (typeof value === "number") {
return formatTime(value / 1000, "YYYY-MM-DD hh:mm:ss")
}
if (/^\d+$/.test(value)) {
return formatTime(Number(value) / 1000, "YYYY-MM-DD hh:mm:ss")
}
return value
}
const normalizeUserInfoList = (res: any): UserInfoRecord[] => {
if (Array.isArray(res)) return res
if (Array.isArray(res?.records)) return res.records
if (res) return [res]
return []
}
const buildUserInfoParams = (id: string | number) => ({
endTime: "",
startTime: "",
size: userInfoPage.size,
page: userInfoPage.current,
systemUser: "",
country: "",
email: "",
userType: "",
ids: [],
occupation: "",
order: "",
orderBy: "",
userName: "",
subscriptionPlanId: id
})
const fetchUserInfo = async () => {
if (currentUserInfoPlanId.value === null) return
userInfoLoading.value = true
userInfoList.value = []
try {
const res = await Https.axiosPost(
Https.httpUrls.getUserInfo,
buildUserInfoParams(currentUserInfoPlanId.value)
)
const records = normalizeUserInfoList(res)
userInfoList.value = records
userInfoPage.total = Number(res?.total ?? records.length)
} catch (error: any) {
message.error(error.message || "Failed to load user info")
console.error(error)
} finally {
userInfoLoading.value = false
}
}
const openUserInfo = async (record: SubscriptionPlan) => {
console.log(record)
// debugger
currentUserInfoPlanId.value = record.id
userInfoPage.current = 1
userInfoModalVisible.value = true
await fetchUserInfo()
}
const changeUserInfoPage = (pagination: any) => {
userInfoPage.current = pagination.current
userInfoPage.size = pagination.pageSize
fetchUserInfo()
}
const customPlanRow = (record: SubscriptionPlan) => {
return {
onClick: (event: MouseEvent) => {
const target = event.target as HTMLElement | null
if (target?.closest(".plan-row-actions")) return
openUserInfo(record)
}
}
}
const historyTable = ref<HTMLElement | null>(null)
const historyTableHeight = ref(0) const historyTableHeight = ref(0)
const minTableBodyHeight = 120
let tableResizeObserver: ResizeObserver | null = null
let tableResizeTimer: ReturnType<typeof window.setTimeout> | null = null
const handleResizeColumn = (w: any, col: any) => { const handleResizeColumn = (w: any, col: any) => {
col.width = w col.width = w
} }
const calculateTableHeight = () => { const getElementOuterHeight = (element: Element | null) => {
nextTick(() => { if (!element) return 0
if (historyTable.value) { const htmlElement = element as HTMLElement
historyTableHeight.value = historyTable.value.clientHeight - 200 const style = window.getComputedStyle(htmlElement)
return (
htmlElement.offsetHeight +
Number.parseFloat(style.marginTop || "0") +
Number.parseFloat(style.marginBottom || "0")
)
} }
const calculateTableHeight = () => {
if (tableResizeTimer) {
window.clearTimeout(tableResizeTimer)
}
tableResizeTimer = window.setTimeout(() => {
nextTick(() => {
const tableWrapper = historyTable.value
if (!tableWrapper) {
tableResizeTimer = null
return
}
const tableHead = tableWrapper.querySelector(".ant-table-thead") ?? null
const pagination = tableWrapper.querySelector(".ant-pagination") ?? null
const tableHeadHeight = getElementOuterHeight(tableHead) || 55
const paginationHeight = getElementOuterHeight(pagination) || 48
const reservedHeight = tableHeadHeight + paginationHeight + 8
historyTableHeight.value = Math.max(
minTableBodyHeight,
tableWrapper.clientHeight - reservedHeight
)
tableResizeTimer = null
}) })
}, 50)
} }
const handleResize = () => { const handleResize = () => {
calculateTableHeight() calculateTableHeight()
} }
const setupTableResizeObserver = () => {
if (!historyTable.value || typeof ResizeObserver === "undefined") return
tableResizeObserver?.disconnect()
tableResizeObserver = new ResizeObserver(() => {
calculateTableHeight()
})
tableResizeObserver.observe(historyTable.value)
const searchCard = historyTable.value
.closest(".subscription-plan")
?.querySelector(".search-card")
if (searchCard) {
tableResizeObserver.observe(searchCard)
}
}
onMounted(async () => { onMounted(async () => {
await getOrganizationList() await getOrganizationList()
await handleSearch() await handleSearch()
calculateTableHeight() calculateTableHeight()
window.addEventListener('resize', handleResize) setupTableResizeObserver()
const list = sessionStorage.getItem('allCountry') window.addEventListener("resize", handleResize)
const list = sessionStorage.getItem("allCountry")
countryList.value = list ? JSON.parse(list) : [] countryList.value = list ? JSON.parse(list) : []
}) })
onActivated(() => {
calculateTableHeight()
})
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize) window.removeEventListener("resize", handleResize)
tableResizeObserver?.disconnect()
if (tableResizeTimer) {
window.clearTimeout(tableResizeTimer)
tableResizeTimer = null
}
}) })
const handleFetchTableData = async () => { const handleFetchTableData = async () => {
tableLoading.value = true tableLoading.value = true
return Https.axiosPost(Https.httpUrls.searchAllSubscribePlan, searchForm) return Https.axiosPost(Https.httpUrls.searchAllSubscribePlan, searchForm)
.then(res => { .then((res) => {
tableData.value = res.records tableData.value = res.records
searchForm.total = res.total searchForm.total = res.total
}) })
.finally(() => { .finally(() => {
tableLoading.value = false tableLoading.value = false
calculateTableHeight()
}) })
} }
const resetFormState = () => { const resetFormState = () => {
formState.name = '' formState.name = ""
formState.currentPeriodStart = '' formState.currentPeriodStart = ""
formState.currentPeriodEnd = '' formState.currentPeriodEnd = ""
formState.organizationId = undefined formState.organizationId = undefined
formState.adminAccId = undefined formState.adminAccId = undefined
formState.creditLimit = null formState.creditLimit = null
@@ -683,26 +973,26 @@ const handleSearch = () => {
} }
const handleReset = () => { const handleReset = () => {
searchForm.name = '' searchForm.name = ""
searchForm.startTime = '' searchForm.startTime = ""
searchForm.endTime = '' searchForm.endTime = ""
searchForm.organizationId = undefined searchForm.organizationId = undefined
searchForm.adminAccId = undefined searchForm.adminAccId = undefined
searchForm.status = [] searchForm.status = []
searchForm.id = '' searchForm.id = ""
searchForm.countryOrRegion = null searchForm.countryOrRegion = null
handleSearch() handleSearch()
} }
const openCreate = () => { const openCreate = () => {
modalTitle.value = 'New Subscription Plan' modalTitle.value = "New Subscription Plan"
isEditMode.value = false isEditMode.value = false
resetFormState() resetFormState()
modalVisible.value = true modalVisible.value = true
} }
const openEdit = (record: SubscriptionPlan) => { const openEdit = (record: SubscriptionPlan) => {
modalTitle.value = 'Edit Subscription Plan' modalTitle.value = "Edit Subscription Plan"
isEditMode.value = true isEditMode.value = true
formState.name = record.name formState.name = record.name
formState.currentPeriodStart = String(record.currentPeriodStart) formState.currentPeriodStart = String(record.currentPeriodStart)
@@ -755,25 +1045,25 @@ const validateForm = (): boolean => {
} }
const requiredFields: FieldRule[] = [ const requiredFields: FieldRule[] = [
{ value: formState.currentPeriodStart, message: 'Please select the start time' }, { value: formState.currentPeriodStart, message: "Please select the start time" },
{ value: formState.currentPeriodEnd, message: 'Please select the end time' }, { value: formState.currentPeriodEnd, message: "Please select the end time" },
{ value: formState.adminAccId, message: 'Please select the admin account' }, { value: formState.adminAccId, message: "Please select the admin account" },
{ {
value: formState.creditLimit, value: formState.creditLimit,
message: 'Please input credit limit', message: "Please input credit limit",
checkNull: true checkNull: true
}, },
{ {
value: formState.accountNum, value: formState.accountNum,
message: 'Please input account number', message: "Please input account number",
checkNull: true checkNull: true
} }
] ]
if (!isEditMode.value) { if (!isEditMode.value) {
requiredFields.push( requiredFields.push(
{ value: formState.name, message: 'Please input the name' }, { value: formState.name, message: "Please input the name" },
{ value: formState.organizationId, message: 'Please select organization' } { value: formState.organizationId, message: "Please select organization" }
) )
} }
@@ -806,7 +1096,7 @@ const handleSubmit = async () => {
res = await Https.axiosPost(Https.httpUrls.createSubscribePlan, params) res = await Https.axiosPost(Https.httpUrls.createSubscribePlan, params)
} }
message.success( message.success(
`${isEditMode.value ? 'Subscription plan updated' : 'Subscription plan created'}` `${isEditMode.value ? "Subscription plan updated" : "Subscription plan created"}`
) )
} catch (error: any) { } catch (error: any) {
message.error(error.message) message.error(error.message)
@@ -835,8 +1125,8 @@ const cancelModal = () => {
const removePlan = (id: number) => { const removePlan = (id: number) => {
tableLoading.value = true tableLoading.value = true
Https.axiosGet(Https.httpUrls.deleteSubscribePlan, { params: { id } }) Https.axiosGet(Https.httpUrls.deleteSubscribePlan, { params: { id } })
.then(res => { .then((res) => {
message.success('Subscription plan deleted') message.success("Subscription plan deleted")
handleReset() handleReset()
}) })
.catch((error: any) => { .catch((error: any) => {
@@ -901,15 +1191,15 @@ const handleOrganizationScroll = (e: any) => {
} }
const handleOrganizationSelect = (value: string) => { const handleOrganizationSelect = (value: string) => {
if (value === 'ADD_ORGANIZATION') { if (value === "ADD_ORGANIZATION") {
// 打开添加组织弹窗 // 打开添加组织弹窗
organizationModalVisible.value = true organizationModalVisible.value = true
// 使用nextTick确保值被重置使其不被选中 // 使用nextTick确保值被重置使其不被选中
nextTick(() => { nextTick(() => {
if (searchForm.organizationId === 'ADD_ORGANIZATION') { if (searchForm.organizationId === "ADD_ORGANIZATION") {
searchForm.organizationId = undefined searchForm.organizationId = undefined
} }
if (formState.organizationId === 'ADD_ORGANIZATION') { if (formState.organizationId === "ADD_ORGANIZATION") {
formState.organizationId = undefined formState.organizationId = undefined
} }
}) })
@@ -918,12 +1208,12 @@ const handleOrganizationSelect = (value: string) => {
const handleOrganizationChange = (value: string) => { const handleOrganizationChange = (value: string) => {
// 如果change事件触发时值是"添加组织",立即重置 // 如果change事件触发时值是"添加组织",立即重置
if (value === 'ADD_ORGANIZATION') { if (value === "ADD_ORGANIZATION") {
nextTick(() => { nextTick(() => {
if (searchForm.organizationId === 'ADD_ORGANIZATION') { if (searchForm.organizationId === "ADD_ORGANIZATION") {
searchForm.organizationId = undefined searchForm.organizationId = undefined
} }
if (formState.organizationId === 'ADD_ORGANIZATION') { if (formState.organizationId === "ADD_ORGANIZATION") {
formState.organizationId = undefined formState.organizationId = undefined
} }
}) })
@@ -932,13 +1222,13 @@ const handleOrganizationChange = (value: string) => {
const cancelOrganizationModal = () => { const cancelOrganizationModal = () => {
organizationModalVisible.value = false organizationModalVisible.value = false
organizationForm.name = '' organizationForm.name = ""
organizationForm.type = undefined organizationForm.type = undefined
} }
const handleCreateOrganization = async () => { const handleCreateOrganization = async () => {
if (!organizationForm.name || !organizationForm.type) { if (!organizationForm.name || !organizationForm.type) {
message.warning('Please fill in name and type') message.warning("Please fill in name and type")
return return
} }
try { try {
@@ -948,7 +1238,7 @@ const handleCreateOrganization = async () => {
type: organizationForm.type type: organizationForm.type
} }
}) })
message.success('Organization created successfully') message.success("Organization created successfully")
cancelOrganizationModal() cancelOrganizationModal()
// 刷新组织列表 // 刷新组织列表
await getOrganizationList() await getOrganizationList()
@@ -960,26 +1250,28 @@ const handleCreateOrganization = async () => {
} }
} }
} catch (error: any) { } catch (error: any) {
message.error(error.message || 'Failed to create organization') message.error(error.message || "Failed to create organization")
console.error(error) console.error(error)
} }
} }
const filterOption = (input: string, option: any) => { const filterOption = (input: string, option: any) => {
const label = option?.label ?? option?.children ?? option?.key?.label ?? '' const label = option?.label ?? option?.children ?? option?.key?.label ?? ""
return String(label).toLowerCase().includes(input.toLowerCase()) return String(label).toLowerCase().includes(input.toLowerCase())
} }
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.subscription-plan { .subscription-plan {
padding: 2rem 2.4rem 3.2rem 0; padding: 2rem 2.4rem 0 0;
display: flex; display: flex;
height: 100%; height: 100%;
min-height: 0;
flex-direction: column; flex-direction: column;
.search-card { .search-card {
margin-bottom: 1.6rem; margin-bottom: 1.6rem;
flex-shrink: 0;
} }
.table-card { .table-card {
@@ -987,13 +1279,15 @@ const filterOption = (input: string, option: any) => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
min-height: 0;
:deep(.ant-card-body) { :deep(.ant-card-body) {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
padding: 2.4rem; padding: 2.4rem 2.4rem 0;
min-height: 0;
} }
.table-card__header { .table-card__header {
@@ -1007,6 +1301,42 @@ const filterOption = (input: string, option: any) => {
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
min-height: 0; min-height: 0;
:deep(.ant-table-wrapper),
:deep(.ant-spin-nested-loading),
:deep(.ant-spin-container) {
height: 100%;
}
:deep(.ant-spin-container) {
display: flex;
flex-direction: column;
overflow: hidden;
}
:deep(.ant-table) {
flex: 1;
min-height: 0;
overflow: hidden;
}
:deep(.ant-table-container) {
height: 100%;
display: flex;
flex-direction: column;
}
:deep(.ant-table-content) {
height: 100%;
}
:deep(.ant-table-body) {
overflow-y: auto !important;
}
:deep(.ant-pagination) {
flex-shrink: 0;
}
} }
.danger-text { .danger-text {
@@ -1026,8 +1356,23 @@ const filterOption = (input: string, option: any) => {
:deep(.ant-table-tbody > tr:hover > td) { :deep(.ant-table-tbody > tr:hover > td) {
background: rgb(202, 202, 202); background: rgb(202, 202, 202);
} }
:deep(.ant-table-tbody > tr) {
cursor: pointer;
}
:deep(.plan-row-actions) {
cursor: default;
} }
} }
}
.user-info-modal {
:deep(.ant-modal-body) {
padding: 2.4rem;
}
}
:deep(.subscriptionPlan_modal) { :deep(.subscriptionPlan_modal) {
.ant-modal-body { .ant-modal-body {
// height: calc(65rem * 1.2); // height: calc(65rem * 1.2);
@@ -1079,11 +1424,69 @@ const filterOption = (input: string, option: any) => {
} }
:deep(.search-form) { :deep(.search-form) {
column-gap: 2rem; --search-label-width: 14rem;
row-gap: 2rem; --search-control-width: 20rem;
align-items: flex-start;
column-gap: 1.8rem;
row-gap: 1.8rem;
.ant-form-item {
min-width: calc(var(--search-label-width) + var(--search-control-width));
margin-right: 0;
margin-bottom: 0;
}
.ant-form-item-label {
flex: 0 0 var(--search-label-width);
max-width: var(--search-label-width);
overflow: visible;
white-space: nowrap;
> label {
white-space: nowrap;
}
}
.ant-form-item-control {
flex: 0 0 var(--search-control-width);
max-width: var(--search-control-width);
}
.ant-input,
.ant-input-affix-wrapper,
.ant-picker,
.ant-select { .ant-select {
width: 100% !important; width: 100% !important;
} }
.search-form__actions {
min-width: auto;
.ant-form-item-control {
flex: 0 0 auto;
max-width: none;
}
}
}
@media (min-width: 1600px) {
:deep(.search-form) {
--search-control-width: 22rem;
}
}
@media (max-width: 760px) {
:deep(.search-form) {
.ant-form-item {
flex: 1 1 100%;
min-width: 100%;
}
.ant-form-item-control {
flex: 1 1 auto;
max-width: calc(100% - var(--search-label-width));
}
}
} }
:deep(.ant-select-dropdown) { :deep(.ant-select-dropdown) {

View File

@@ -1737,7 +1737,7 @@ export default {
GetStarted: '开始体验', GetStarted: '开始体验',
}, },
SellerListEdit:{ SellerListEdit:{
saveDraft: "保存草稿", saveDraft: "全部保存",
publish: "发布", publish: "发布",
sketch: "线稿图", sketch: "线稿图",
mainProductImage: "产品主图", mainProductImage: "产品主图",

View File

@@ -1788,7 +1788,7 @@ export default {
GetStarted: 'Get Started', GetStarted: 'Get Started',
}, },
SellerListEdit:{ SellerListEdit:{
saveDraft:'Save Draft', saveDraft:'Save All New',
publish:'Publish', publish:'Publish',
sketch:'Sketch', sketch:'Sketch',
mainProductImage:'Main Product Image', mainProductImage:'Main Product Image',

View File

@@ -144,7 +144,6 @@ const open = (url, callback, options, origin) => {
coverOrigin.value = origin coverOrigin.value = origin
data.url = origin[0].url data.url = origin[0].url
} }
console.log("-------", origin)
show.value = true show.value = true
} }
const onCancel = () => { const onCancel = () => {

View File

@@ -204,23 +204,23 @@
.cropper-box-canvas { .cropper-box-canvas {
background-color: #ffffff; background-color: #ffffff;
img { // img {
height: 100%; // height: 100%;
} // }
} }
} }
&.is-cover { &.is-cover {
:deep(.vue-cropper) { :deep(.vue-cropper) {
overflow: hidden; overflow: hidden;
} }
:deep(.cropper-box-canvas) { // :deep(.cropper-box-canvas) {
width: 31.1rem !important; // width: 31.1rem !important;
left: 50% !important; // left: 50% !important;
transform: translateX(-50%) !important; // transform: translateX(-50%) !important;
img { // img {
display: none; // display: none;
} // }
} // }
} }
} }

View File

@@ -0,0 +1,86 @@
<script setup lang="ts">
import type { ListingItem } from "../types"
defineProps<{
sketchList: ListingItem["sketchList"]
}>()
const emit = defineEmits<{
(e: "crop", data: string | null, type: "apparel", index?: number): void
}>()
</script>
<template>
<div class="apparel-container">
<div class="title">
<span class="main-title">{{ $t("SellerListEdit.apparelSketchTitle") }}</span>
<span class="sub-title">{{ $t("SellerListEdit.apparelSketchSubTitle") }}</span>
</div>
<div class="sketch-list-container flex">
<div
v-for="(item, index) in sketchList"
:key="index"
class="sketch-element flex flex-center"
>
<img class="img-src" :src="item.url || ''" alt="" />
<div class="crop-tool flex flex-center" @click="emit('crop', item.url, 'apparel', index)">
<SvgIcon name="CCrop" color="#fff" size="12" />
</div>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.apparel-container {
margin-top: 3rem;
.title {
font-size: 1.4rem;
margin-bottom: 0.8rem;
.main-title {
font-weight: 400;
font-style: bold;
&::after {
content: "*";
color: #df2b2c;
}
}
.sub-title {
font-size: 1.2rem;
color: #999;
}
}
.sketch-list-container {
column-gap: 1rem;
flex-wrap: wrap;
.sketch-element {
width: 10rem;
height: 14.6rem;
border: 0.15rem solid #c7c7c7;
border-radius: 1.2rem;
position: relative;
overflow: hidden;
.img-src {
width: 100%;
}
.crop-tool {
position: absolute;
top: 0.4rem;
right: 0.4rem;
width: 2rem;
height: 2rem;
border-radius: 50%;
background-color: #000000;
}
}
}
}
</style>

View File

@@ -0,0 +1,222 @@
<script setup lang="ts">
import Radio from "./Radio.vue"
import type { RadioOption } from "../types"
defineProps<{
productName: string
price: string
desc: string
gender: string
category: string[] | null
genderOptions: RadioOption[]
categoryOptions: RadioOption[]
}>()
const emit = defineEmits<{
(e: "update:productName", value: string): void
(e: "update:price", value: string): void
(e: "update:desc", value: string): void
(e: "update:gender", value: string): void
(e: "update:category", value: string[] | null): void
}>()
</script>
<template>
<div class="form-container flex flex-col">
<div class="form-item">
<div class="form-item-label required">
{{ $t("SellerListEdit.productName") }}
</div>
<div class="form-item-value product-name">
<a-input
:value="productName"
show-count
placeholder="Enter product name"
:bordered="false"
:maxlength="60"
@update:value="emit('update:productName', $event)"
/>
</div>
</div>
<div class="form-item">
<div class="form-item-label required">{{ $t("SellerListEdit.price") }}</div>
<div class="form-item-value price flex align-center">
<span>HK$</span>
<a-input
:value="price"
placeholder="0.00"
:bordered="false"
@update:value="emit('update:price', $event)"
/>
</div>
</div>
<div class="form-item">
<div class="form-item-label required">
{{ $t("SellerListEdit.productDescription") }}
</div>
<div class="form-item-value desc">
<a-textarea
:value="desc"
show-count
:rows="4"
placeholder="Enter product description"
:bordered="false"
:maxlength="500"
@update:value="emit('update:desc', $event)"
/>
</div>
</div>
<div class="form-item">
<div class="form-item-label required">
{{ $t("SellerListEdit.designFor") }}
</div>
<div class="form-item-value no-border">
<Radio
:options="genderOptions"
:model-value="gender"
@update:model-value="emit('update:gender', $event)"
/>
</div>
</div>
<div class="form-item">
<div class="form-item-label with-tip">
<span class="required">{{ $t("SellerListEdit.productCategory") }}</span>
<span class="help-text">{{ $t("SellerListEdit.categoryTips") }}</span>
</div>
<div class="form-item-value no-border">
<Radio
multiple
:options="categoryOptions"
:model-value="category"
@update:model-value="emit('update:category', $event)"
/>
</div>
</div>
<div class="license-note flex align-center">
<img src="@/assets/images/seller/tips.png" class="info-icon" />
<div class="note-copy">
{{ $t("SellerListEdit.policy") }}
<a href="javascript:void(0)">{{ $t("SellerListEdit.learnMore") }}</a>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.required {
&::after {
content: "*";
color: #df2b2c;
margin-left: 0.4rem;
}
}
.form-container {
row-gap: 3rem;
.form-item {
.form-item-label {
font-size: 1.4rem;
font-weight: 400;
font-style: bold;
margin-bottom: 0.6rem;
line-height: 1.5;
&.with-tip {
display: flex;
align-items: center;
column-gap: 0.8rem;
}
.help-text {
font-size: 1rem;
color: #999;
}
}
.form-item-value {
border: 0.16rem solid #d1d1d1;
border-radius: 1.2rem;
position: relative;
padding: 1.6rem;
box-sizing: border-box;
font-size: 1.2rem;
color: #000;
&.no-border {
border: none;
padding: 0;
}
&.price {
column-gap: 0.6rem;
}
:deep(.ant-input),
:deep(.ant-input-affix-wrapper),
:deep(.ant-input-textarea textarea) {
border: none;
padding: 0;
font-size: 1.2rem;
color: #000;
box-shadow: none;
}
:deep(.ant-input) {
line-height: 1.5;
}
:deep(.ant-input-show-count-suffix) {
color: #df2c2c;
font-size: 1rem;
}
:deep(textarea.ant-input) {
resize: none;
min-height: 5.4rem;
padding-bottom: 1.8rem;
line-height: 1.5;
}
:deep(.ant-input-textarea-show-count) {
position: relative;
&::after {
float: none;
position: absolute;
color: #df2c2c;
bottom: 0;
right: 1.6rem;
}
}
}
}
}
.license-note {
padding: 1.6rem;
column-gap: 1.6rem;
background: #f7f7f7;
border-radius: 0.8rem;
.info-icon {
width: 2.4rem;
height: 2.4rem;
flex-shrink: 0;
}
.note-copy {
font-size: 1.2rem;
line-height: 1.5;
color: #000;
font-weight: 400;
font-style: medium;
a {
color: #0080ed;
text-decoration: underline;
margin-left: 0.4rem;
}
}
}
</style>

View File

@@ -0,0 +1,154 @@
<script setup lang="ts">
import { computed } from "vue"
import type { ListingItem } from "../types"
const props = defineProps<{
imageList: ListingItem["prodImageList"]
firstSelectedIndex: number | null
}>()
const emit = defineEmits<{
(e: "select", index: number): void
}>()
const selectedCount = computed(() => props.imageList.filter((item) => item.selected).length)
</script>
<template>
<div class="product-image-list-container">
<div class="title flex align-center space-between">
<div class="title-left">
<span class="main-title">{{ $t("SellerListEdit.productImageMainTitle") }}</span>
<span class="sub-title">{{ $t("SellerListEdit.productImageSubTitle") }}</span>
</div>
<div class="title-right">{{ selectedCount }}/{{ imageList.length }} selected</div>
</div>
<div class="product-image-list flex">
<div
v-for="(item, index) in imageList"
:key="index"
class="product-image-item flex flex-center"
:class="{ selected: item.selected }"
@click="emit('select', index)"
>
<img
v-if="item.selected"
src="@/assets/images/seller/checked.png"
class="checked"
alt=""
/>
<img class="img-src" :src="item.url" alt="" />
<div v-if="item.selected && index === firstSelectedIndex" class="main-pic">
main
</div>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.product-image-list-container {
margin-top: 3rem;
.title {
font-size: 1.4rem;
margin-bottom: 1.2rem;
.main-title {
font-weight: 400;
font-style: bold;
}
.sub-title {
font-size: 1.2rem;
color: #999;
}
.title-right {
color: #585858;
font-size: 1.4rem;
}
}
.product-image-list {
overflow-x: auto;
overflow-y: hidden;
column-gap: 0.8rem;
max-width: 80.2rem;
padding-bottom: 1.2rem;
&::-webkit-scrollbar {
height: 0.8rem;
}
&::-webkit-scrollbar-track {
background: #d9d9d9;
border-radius: 0.8rem;
}
&::-webkit-scrollbar-thumb {
background: #000000;
border-radius: 0.8rem;
}
.product-image-item {
width: 11.6rem;
height: 20.6rem;
border-radius: 1rem;
border: 0.15rem solid #c7c7c7;
position: relative;
cursor: pointer;
overflow: hidden;
flex-shrink: 0;
&::after {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
content: "";
background-color: #fcfcfc;
opacity: 0.7;
border-radius: 1rem;
}
&.selected {
border-color: #000;
&::after {
display: none;
}
}
.checked {
width: 2rem;
height: 2rem;
position: absolute;
top: 0.8rem;
right: 0.8rem;
z-index: 1;
}
.img-src {
height: 100%;
}
.main-pic {
position: absolute;
height: 2.4rem;
line-height: 2.4rem;
left: 0.8rem;
right: 0.8rem;
bottom: 0.8rem;
z-index: 1;
background: rgba(0, 0, 0, 0.8);
color: #fff;
font-size: 1.4rem;
border-radius: 1.2rem;
text-align: center;
}
}
}
}
</style>

View File

@@ -8,7 +8,7 @@
'radio-button', 'radio-button',
{ {
'is-active': multiple 'is-active': multiple
? selectedValues.includes(item.key) ? selectedValues.includes(normalizeValue(item.key))
: modelValue === item.key : modelValue === item.key
} }
]" ]"
@@ -30,7 +30,12 @@
} }
const props = defineProps<{ const props = defineProps<{
modelValue: string | number | boolean | Array<string | number | boolean> | null modelValue:
| string
| number
| boolean
| Array<string | number | boolean | null | undefined>
| null
options: Option[] // 按钮选项数组 options: Option[] // 按钮选项数组
multiple?: boolean // 是否支持多选,默认为 false multiple?: boolean // 是否支持多选,默认为 false
}>() }>()
@@ -41,6 +46,9 @@
const multiple = props.multiple === true const multiple = props.multiple === true
const normalizeValue = (value: any) =>
typeof value === "string" ? value.toLocaleLowerCase() : value
const selectedValues = computed(() => { const selectedValues = computed(() => {
if (!multiple) { if (!multiple) {
return typeof props.modelValue === "undefined" || props.modelValue === null return typeof props.modelValue === "undefined" || props.modelValue === null
@@ -48,19 +56,30 @@
: [props.modelValue] : [props.modelValue]
} }
return Array.isArray(props.modelValue) ? props.modelValue : [] return Array.isArray(props.modelValue)
? props.modelValue
.filter((value) => value !== null && typeof value !== "undefined")
.map(normalizeValue)
: []
}) })
const selectOption = (value: any) => { const selectOption = (value: any) => {
if (multiple) { if (multiple) {
const current = Array.isArray(props.modelValue) ? [...props.modelValue] : [] const selectedValue = normalizeValue(value)
const index = current.indexOf(value) const current = Array.isArray(props.modelValue)
? props.modelValue
.filter((item) => item !== null && typeof item !== "undefined")
.map(normalizeValue)
: []
const index = current.indexOf(selectedValue)
if (index >= 0) { if (index >= 0) {
current.splice(index, 1) current.splice(index, 1)
} else { } else {
current.push(value.toLocaleLowerCase) current.push(selectedValue)
} }
emit("update:modelValue", current)
emit("update:modelValue", current.length ? current : null)
return return
} }

View File

@@ -0,0 +1,157 @@
<script setup lang="ts">
import type { TopImageType } from "../types"
defineProps<{
images: Record<TopImageType, string | null>
}>()
const emit = defineEmits<{
(e: "crop", data: string | null, type: TopImageType): void
}>()
const topImageList: TopImageType[] = ["sketch", "mainProductImage", "cover"]
const topImageTitleMap: Record<TopImageType, string> = {
sketch: "SellerListEdit.sketch",
mainProductImage: "SellerListEdit.mainProductImage",
cover: "SellerListEdit.cover"
}
</script>
<template>
<div class="main-image-container flex">
<div
v-for="type in topImageList"
:key="type"
:class="`main-image-item flex flex-col align-center ${type}`"
>
<div class="title" :class="{ required: type !== 'mainProductImage' }">
{{ $t(topImageTitleMap[type]) }}
</div>
<div class="sketch-item flex flex-center" :class="type">
<div
v-if="images[type]"
class="crop-tool flex flex-center"
@click="emit('crop', images[type], type)"
>
<SvgIcon name="CCrop" color="#fff" size="12" />
</div>
<img
v-if="images[type]"
:src="images[type] || ''"
class="sketch-img"
:class="type"
alt=""
/>
<div v-else class="trigger flex flex-col align-center">
<div
v-if="type === 'cover'"
class="cover-trigger flex flex-col align-center"
@click="emit('crop', null, 'cover')"
>
<SvgIcon class="trigger-icon" name="CCrop" color="#585858" size="24" />
<div class="trigger-tips">
{{ $t("SellerListEdit.cropDesc") }}
</div>
</div>
<template v-else>
<div class="trigger-img placeholder"></div>
<div class="trigger-tips">
{{ $t("SellerListEdit.productImageDesc") }}
</div>
</template>
</div>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.c-svg {
width: initial;
height: initial;
}
.required {
&::after {
content: "*";
color: #df2b2c;
margin-left: 0.4rem;
}
}
.main-image-container {
column-gap: 3.5rem;
.main-image-item {
flex-shrink: 0;
.title {
font-size: 1.4rem;
margin-bottom: 0.8rem;
text-align: center;
}
.sketch-item {
width: 11.6rem;
height: 20.4rem;
border: 0.15rem solid #d1d1d1;
border-radius: 1rem;
position: relative;
background-color: #f6f6f6;
overflow: hidden;
&.cover {
width: 16.2rem;
background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' rx='11' ry='11' fill='none' stroke='%23D1D1D1' stroke-width='1.5' stroke-dasharray='8%2c 5' stroke-linecap='square'/%3e%3c/svg%3e");
border: none;
}
.crop-tool {
position: absolute;
top: 0.8rem;
right: 0.8rem;
width: 2rem;
height: 2rem;
border-radius: 50%;
background-color: #000000;
z-index: 1;
cursor: pointer;
}
.sketch-img {
height: 100%;
&.sketch {
height: initial;
width: 100%;
}
}
.trigger {
cursor: pointer;
height: 100%;
padding: 6rem 2rem 0;
&,
.cover-trigger {
row-gap: 1.2rem;
}
.placeholder {
width: 2.4rem;
height: 2.4rem;
border-radius: 0.6rem;
background: linear-gradient(135deg, #efefef 0%, #cdcdcd 100%);
}
.trigger-tips {
font-size: 1.2rem;
text-align: center;
color: #585858;
line-height: 1.3;
}
}
}
}
}
</style>

View File

@@ -28,191 +28,32 @@
</seller-header> </seller-header>
<div class="edit-detail-content flex"> <div class="edit-detail-content flex">
<div class="left"> <div class="left">
<div class="main-image-container flex"> <TopImageSection :images="previewImageMap" @crop="handleClickCrop" />
<div <ProductImageList
v-for="type in topImageList" :image-list="prodImgList"
:key="type" :first-selected-index="firstSelectedIndex"
:class="`main-image-item flex flex-col align-center ${type}`" @select="handleSelectProdImg"
>
<div class="title" :class="{ required: type !== 'mainProductImage' }">
{{ $t(topImageTitleMap[type]) }}
</div>
<div class="sketch-item flex flex-center" :class="type">
<div
v-if="previewImageMap[type]"
class="crop-tool flex flex-center"
@click="handleClickCrop(previewImageMap[type], type)"
>
<SvgIcon name="CCrop" color="#fff" size="12" />
</div>
<img
v-if="previewImageMap[type]"
:src="previewImageMap[type]"
class="sketch-img"
:class="type"
alt=""
/> />
<div v-else class="trigger flex flex-col align-center"> <ApparelSketchList
<div :sketch-list="currentListing.sketchList"
v-if="type === 'cover'" @crop="handleClickCrop"
class="cover-trigger flex flex-col align-center"
@click="handleClickCrop(null, 'cover')"
>
<SvgIcon
class="trigger-icon"
name="CCrop"
color="#585858"
size="24"
/> />
<div class="trigger-tips">
{{ $t("SellerListEdit.cropDesc") }}
</div>
</div>
<template v-else>
<div class="trigger-img placeholder"></div>
<div class="trigger-tips">
{{ $t("SellerListEdit.productImageDesc") }}
</div>
</template>
</div>
</div>
</div>
</div>
<div class="product-image-list-container">
<div class="title flex align-center space-between">
<div class="title-left">
<span class="main-title">{{
$t("SellerListEdit.productImageMainTitle")
}}</span>
<span class="sub-title">{{
$t("SellerListEdit.productImageSubTitle")
}}</span>
</div>
<div class="title-right">
{{ selectedProdImgs }}/{{ prodImgList.length }} selected
</div>
</div>
<div class="product-image-list flex">
<div
v-for="(item, index) in prodImgList"
:key="index"
class="product-image-item flex flex-center"
:class="{ selected: item.selected }"
@click="handleSelectProdImg(index)"
>
<img
v-if="item.selected"
src="@/assets/images/seller/checked.png"
class="checked"
alt=""
/>
<img class="img-src" :src="item.url" alt="" />
<div
v-if="item.selected && index === firstSelectedIndex"
class="main-pic"
>
main
</div>
</div>
</div>
</div>
<div class="apparel-container">
<div class="title">
<span class="main-title">{{
$t("SellerListEdit.apparelSketchTitle")
}}</span>
<span class="sub-title">
{{ $t("SellerListEdit.apparelSketchSubTitle") }}</span
>
</div>
<div class="sketch-list-container flex">
<div
v-for="(item, index) in selectList[currentIndex].sketchList"
:key="index"
class="sketch-element flex flex-center"
>
<img class="img-src" :src="item.url" alt="" />
<div
class="crop-tool flex flex-center"
@click="handleClickCrop(item.url, 'apparel')"
>
<SvgIcon name="CCrop" color="#fff" size="12" />
</div>
</div>
</div>
</div>
</div> </div>
<div class="right"> <div class="right">
<div class="form-container flex flex-col"> <ListingForm
<div class="form-item"> :product-name="currentListing.productName"
<div class="form-item-label required"> :price="currentListing.price"
{{ $t("SellerListEdit.productName") }} :desc="currentListing.desc"
</div> :gender="currentListing.gender"
<div class="form-item-value product-name"> :category="currentListing.category"
<a-input :gender-options="genderOptions"
v-model:value="currentListing.productName" :category-options="categoryOptions"
show-count @update:product-name="currentListing.productName = $event"
placeholder="Enter product name" @update:price="currentListing.price = $event"
:bordered="false" @update:desc="currentListing.desc = $event"
:maxlength="60" @update:gender="currentListing.gender = $event"
@update:category="currentListing.category = $event"
/> />
</div>
</div>
<div class="form-item">
<div class="form-item-label required">{{ $t("SellerListEdit.price") }}</div>
<div class="form-item-value price flex align-center">
<span>HK$</span>
<a-input
v-model:value="currentListing.price"
placeholder="0.00"
:bordered="false"
/>
</div>
</div>
<div class="form-item">
<div class="form-item-label required">
{{ $t("SellerListEdit.productDescription") }}
</div>
<div class="form-item-value desc">
<a-textarea
v-model:value="currentListing.desc"
show-count
:rows="4"
placeholder="Enter product description"
:bordered="false"
:maxlength="500"
/>
</div>
</div>
<div class="form-item">
<div class="form-item-label required">
{{ $t("SellerListEdit.designFor") }}
</div>
<div class="form-item-value no-border">
<Radio :options="genderOptions" v-model="currentListing.gender" />
</div>
</div>
<div class="form-item">
<div class="form-item-label with-tip">
<span class="required">{{ $t("SellerListEdit.productCategory") }}</span>
<span class="help-text">{{ $t("SellerListEdit.categoryTips") }}</span>
</div>
<div class="form-item-value no-border">
<Radio
multiple
:options="categoryOptions"
v-model="currentListing.category"
/>
</div>
</div>
<div class="license-note flex align-center">
<img src="@/assets/images/seller/tips.png" class="info-icon" />
<div class="note-copy">
{{ $t("SellerListEdit.policy") }}
<a href="javascript:void(0)">{{ $t("SellerListEdit.learnMore") }}</a>
</div>
</div>
</div>
<div class="page-control flex align-center" v-if="selectList.length > 1"> <div class="page-control flex align-center" v-if="selectList.length > 1">
<a-pagination <a-pagination
v-model:current="currentPage" v-model:current="currentPage"
@@ -238,13 +79,16 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch, onMounted } from "vue" import { computed, ref, onMounted } from "vue"
import { useRouter } from "vue-router" import { useRouter } from "vue-router"
import { useI18n } from "vue-i18n" import { useI18n } from "vue-i18n"
import { message } from "ant-design-vue" import { message } from "ant-design-vue"
import SellerHeader from "../../seller-header.vue" import SellerHeader from "../../seller-header.vue"
import Radio from "./components/Radio.vue"
import ImageClipDialog from "../../BrandProfile/image-clip-dialog.vue" import ImageClipDialog from "../../BrandProfile/image-clip-dialog.vue"
import ApparelSketchList from "./components/ApparelSketchList.vue"
import ListingForm from "./components/ListingForm.vue"
import ProductImageList from "./components/ProductImageList.vue"
import TopImageSection from "./components/TopImageSection.vue"
import { useStore } from "vuex" import { useStore } from "vuex"
import { import {
fetchSketchDetail, fetchSketchDetail,
@@ -252,6 +96,13 @@
fetchListingDetailById, fetchListingDetailById,
fetchUpdateListing fetchUpdateListing
} from "./api" } from "./api"
import type {
ListingDetailImage,
ListingDetailResponse,
ListingItem,
RadioOption,
StatusType
} from "./types"
const ROUTER = useRouter() const ROUTER = useRouter()
const { t } = useI18n() const { t } = useI18n()
@@ -264,31 +115,6 @@
const STORE = useStore() const STORE = useStore()
type CategoryOption = {
label: string
value: string
}
type ListingItem = {
designItemId: number | string | null
sketch: string | null
mainProductImage: string
cover: string
productImage: string[]
apparelSketch: string[]
productName: string
price: string
desc: string
gender: string
category: string[]
prodImageList: Array<{
url: string
selected?: boolean
}>
sketchList: Array<{ url: string | null }>
}
type StatusType = "draft" | "publish"
const createListingItem = ( const createListingItem = (
sketch: string | null = null, sketch: string | null = null,
designItemId: number | string | null = null designItemId: number | string | null = null
@@ -308,16 +134,9 @@
sketchList: [] sketchList: []
}) })
const topImageList = ["sketch", "mainProductImage", "cover"] as const
const topImageTitleMap: Record<(typeof topImageList)[number], string> = {
sketch: "SellerListEdit.sketch",
mainProductImage: "SellerListEdit.mainProductImage",
cover: "SellerListEdit.cover"
}
const genderOptions = STORE.state.UserHabit?.sex.value || [] const genderOptions = STORE.state.UserHabit?.sex.value || []
const fallbackCategoryOptions: Record<string, CategoryOption[]> = { const fallbackCategoryOptions: Record<string, RadioOption[]> = {
MALE: STORE.state.UserHabit?.MalePosition || [], MALE: STORE.state.UserHabit?.MalePosition || [],
FEMALE: STORE.state.UserHabit?.FemalePosition || [] FEMALE: STORE.state.UserHabit?.FemalePosition || []
} }
@@ -343,21 +162,96 @@
cover: currentListing.value.cover cover: currentListing.value.cover
})) }))
const firstSelectedIndex = ref(null) //显示main标签的图片索引 const firstSelectedIndex = ref<number | null>(null) //显示main标签的图片索引
const selectedProdImgs = computed(() => {
return prodImgList.value.filter((item) => item.selected).length const getSortedDetailImages = (images: ListingDetailImage[] = []) => {
return [...images].sort((prev, next) => (prev.sortOrder ?? 0) - (next.sortOrder ?? 0))
}
const getImageSelected = (value: ListingDetailImage["isSelected"]) =>
value === true || value === 1 || value === "1"
const normalizeDetailGender = (value: ListingDetailResponse["designFor"]) => {
const gender = String(value || "").toUpperCase()
return gender === "MALE" || gender === "FEMALE" ? gender : "FEMALE"
}
const normalizeDetailCategory = (
value: ListingDetailResponse["productCategory"]
): ListingItem["category"] => {
const categories = Array.isArray(value) ? value : value ? [value] : []
const normalized = categories
.filter((category) => category !== null && typeof category !== "undefined")
.map((category) => String(category).toLowerCase())
return normalized.length ? normalized : null
}
const createListingItemFromDetail = (detail: ListingDetailResponse): ListingItem => {
const listing = createListingItem()
listing.productName = detail.title || ""
listing.price =
detail.price === null || typeof detail.price === "undefined" ? "" : String(detail.price)
listing.desc = detail.description || ""
listing.gender = normalizeDetailGender(detail.designFor)
listing.category = normalizeDetailCategory(detail.productCategory)
getSortedDetailImages(detail.images || []).forEach((image) => {
const imageUrl = image.imageUrl || ""
if (!imageUrl) return
if (image.category === "cover") {
listing.cover = imageUrl
return
}
if (image.category === "sketch") {
listing.sketch = imageUrl
return
}
if (image.category === "mainProductImage") {
listing.mainProductImage = imageUrl
return
}
if (image.category === "main_product" || image.category === "product") {
listing.prodImageList.push({
url: imageUrl,
selected: getImageSelected(image.isSelected)
}) })
return
}
if (image.category === "apparel") {
listing.sketchList.push({ url: imageUrl })
}
})
if (!listing.mainProductImage) {
listing.mainProductImage =
listing.prodImageList.find((item) => item.selected)?.url || ""
}
listing.productImage = listing.prodImageList.map((item) => item.url)
listing.apparelSketch = listing.sketchList
.map((item) => item.url)
.filter((url): url is string => Boolean(url))
return listing
}
const handleSelectProdImg = (index: number) => { const handleSelectProdImg = (index: number) => {
const target = prodImgList.value[index] const target = prodImgList.value[index]
const willSelect = !target.selected const willSelect = !target.selected
target.selected = willSelect target.selected = willSelect
if (willSelect && !currentListing.value.mainProductImage) { if (willSelect && firstSelectedIndex.value === null) {
currentListing.value.mainProductImage = target.url currentListing.value.mainProductImage = target.url
firstSelectedIndex.value = index firstSelectedIndex.value = index
return
} }
if (!willSelect && currentListing.value.mainProductImage === target.url) { if (!willSelect && currentListing.value.mainProductImage === target.url) {
@@ -367,7 +261,12 @@
} }
const cropType = ref("") const cropType = ref("")
const handleClickCrop = (data, type, list = []) => { const handleClickCrop = (data: any, type: string, paramThree: any = []) => {
// 处理来自TopImageSection的调用: (data, type, list)
// 处理来自ApparelSketchList的调用: (data, type, index)
const index = typeof paramThree === 'number' ? paramThree : undefined
const list = Array.isArray(paramThree) ? paramThree : []
// console.log(data, type) // console.log(data, type)
// console.log(selectList.value[currentIndex.value]) // console.log(selectList.value[currentIndex.value])
let origin = [] let origin = []
@@ -392,8 +291,11 @@
(file) => { (file) => {
// console.log(file) // console.log(file)
uploadFile(file).then((res) => { uploadFile(file).then((res) => {
console.log(res) if (type === "apparel" && typeof index !== "undefined") {
selectList.value[currentIndex.value].sketchList[index].url = res
} else {
selectList.value[currentIndex.value][type] = res selectList.value[currentIndex.value][type] = res
}
}) })
}, },
{ ratio, isPreview: true, title: titleList[type], isProduct: true }, { ratio, isPreview: true, title: titleList[type], isProduct: true },
@@ -450,35 +352,40 @@
const handleSaveForm = async (type: StatusType) => { const handleSaveForm = async (type: StatusType) => {
const paramsList = [] const paramsList = []
selectList.value.forEach((item) => { selectList.value.forEach((item: ListingItem) => {
const params = { const params = {
id: null, id: null,
title: selectList.value[currentIndex.value].productName, title: item.productName,
description: selectList.value[currentIndex.value].desc, description: item.desc,
price: selectList.value[currentIndex.value].price, price: item.price,
status: type === "draft" ? 0 : 1, status: type === "draft" ? 0 : 1,
images: [], images: [],
designFor: selectList.value[currentIndex.value].gender.toLowerCase, designFor: item.gender.toLowerCase,
productCategory: selectList.value[currentIndex.value].category productCategory: item.category
} }
//
topImageList.forEach((el) => { ;["sketch", "cover"].forEach((el) => {
params.images.push({ params.images.push({
category: el, category: el,
imageUrl: selectList.value[currentIndex.value][el], imageUrl: item[el],
isSelected: 1 isSelected: 1
}) })
}) })
selectList.value[currentIndex.value].prodImageList.forEach((item) => { if (item.mainProductImage) {
if (item.selected) {
params.images.push({ params.images.push({
category: "main_product", category: 'main_product',
imageUrl: item.url, imageUrl: item.mainProductImage,
isSelected: Number(item.selected) isSeleted:1
}) })
} }
item.prodImageList.forEach((item) => {
params.images.push({
category: "product",
imageUrl: item.url,
isSelected: Number(!!item.selected)
}) })
selectList.value[currentIndex.value].sketchList.forEach((item) => { })
item.sketchList.forEach((item) => {
params.images.push({ params.images.push({
category: "apparel", category: "apparel",
imageUrl: item.url, imageUrl: item.url,
@@ -488,9 +395,14 @@
paramsList.push(params) paramsList.push(params)
}) })
console.log(paramsList) console.log(paramsList)
fetchUpdateListing(paramsList) debugger
await fetchUpdateListing(paramsList)
} }
const handleClickMenu = async (status: StatusType) => { const handleClickMenu = async (status: StatusType) => {
if (status === "draft" && !selectList.value[currentIndex.value].cover) {
message.error("请先完成封面制作")
return
}
if (status === "publish" && !validatePublishRequired()) return if (status === "publish" && !validatePublishRequired()) return
await handleSaveForm(status) await handleSaveForm(status)
@@ -516,14 +428,27 @@
})) }))
}) })
}) })
// fetchListingDetailById(itemId.value).then(res => { }
// console.log('iddetail',res)
// }) const handleGetDetailById = () => {
fetchListingDetailById(itemId.value).then((res: ListingDetailResponse) => {
const listing = createListingItemFromDetail(res)
const selectedIndex = listing.prodImageList.findIndex((item) => item.selected)
currentPage.value = 1
selectList.value = [listing]
firstSelectedIndex.value = selectedIndex === -1 ? null : selectedIndex
})
} }
onMounted(() => { onMounted(() => {
const designItemIds = history.state?.designItemIds || [] const data = history.state
if (data?.type === "edit") {
itemId.value = history.state?.id || "" itemId.value = history.state?.id || ""
handleGetDetailById()
} else {
const designItemIds = history.state?.designItemIds || []
if (!designItemIds.length) return if (!designItemIds.length) return
currentPage.value = 1 currentPage.value = 1
@@ -531,8 +456,9 @@
createListingItem(item.designOutfitUrl, item.designItemId) createListingItem(item.designOutfitUrl, item.designItemId)
) )
const list = designItemIds.map((el) => el.designItemId) const list = designItemIds.map((el) => el.designItemId)
console.log("list", list.length, list) // console.log("list", list.length, list)
handleFetchItemDetial(list) handleFetchItemDetial(list)
}
}) })
</script> </script>
@@ -602,362 +528,10 @@
height: 0; height: 0;
} }
.required {
&::after {
content: "*";
color: #df2b2c;
margin-left: 0.4rem;
}
}
.left {
// flex: 1;
// min-width: 0;
.main-image-container {
// max-width: 80.2rem;
column-gap: 3.5rem;
.main-image-item {
flex-shrink: 0;
.title {
font-size: 1.4rem;
margin-bottom: 0.8rem;
text-align: center;
}
.sketch-item {
width: 11.6rem;
height: 20.4rem;
border: 0.15rem solid #d1d1d1;
border-radius: 1rem;
position: relative;
background-color: #f6f6f6;
overflow: hidden;
&.cover {
width: 16.2rem;
background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' rx='11' ry='11' fill='none' stroke='%23D1D1D1' stroke-width='1.5' stroke-dasharray='8%2c 5' stroke-linecap='square'/%3e%3c/svg%3e");
border: none;
}
.crop-tool {
position: absolute;
top: 0.8rem;
right: 0.8rem;
width: 2rem;
height: 2rem;
border-radius: 50%;
background-color: #000000;
z-index: 1;
cursor: pointer;
}
.sketch-img {
height: 100%;
&.sketch {
height: initial;
width: 100%;
}
}
.trigger {
cursor: pointer;
height: 100%;
padding: 6rem 2rem 0;
&,
.cover-trigger {
row-gap: 1.2rem;
}
.placeholder {
width: 2.4rem;
height: 2.4rem;
border-radius: 0.6rem;
background: linear-gradient(135deg, #efefef 0%, #cdcdcd 100%);
}
.trigger-tips {
font-size: 1.2rem;
text-align: center;
color: #585858;
line-height: 1.3;
}
}
}
}
}
.product-image-list-container {
margin-top: 3rem;
.title {
font-size: 1.4rem;
margin-bottom: 1.2rem;
.main-title {
font-weight: 400;
font-style: bold;
}
.sub-title {
font-size: 1.2rem;
color: #999;
}
.title-right {
color: #585858;
font-size: 1.4rem;
}
}
.product-image-list {
overflow-x: auto;
overflow-y: hidden;
column-gap: 0.8rem;
max-width: 80.2rem;
padding-bottom: 1.2rem;
&::-webkit-scrollbar {
height: 0.8rem;
}
&::-webkit-scrollbar-track {
background: #d9d9d9;
border-radius: 0.8rem;
}
&::-webkit-scrollbar-thumb {
background: #000000;
border-radius: 0.8rem;
}
.product-image-item {
width: 11.6rem;
height: 20.6rem;
border-radius: 1rem;
border: 0.15rem solid #c7c7c7;
position: relative;
cursor: pointer;
overflow: hidden;
flex-shrink: 0;
&::after {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
content: "";
background-color: #fcfcfc;
opacity: 0.7;
border-radius: 1rem;
}
&.selected {
border-color: #000;
&::after {
display: none;
}
}
.checked {
width: 2rem;
height: 2rem;
position: absolute;
top: 0.8rem;
right: 0.8rem;
z-index: 1;
}
.img-src {
height: 100%;
}
.main-pic {
position: absolute;
height: 2.4rem;
line-height: 2.4rem;
left: 0.8rem;
right: 0.8rem;
bottom: 0.8rem;
z-index: 1;
background: rgba(0, 0, 0, 0.8);
color: #fff;
font-size: 1.4rem;
border-radius: 1.2rem;
text-align: center;
}
}
}
}
.apparel-container {
margin-top: 3rem;
.title {
font-size: 1.4rem;
margin-bottom: 0.8rem;
.main-title {
font-weight: 400;
font-style: bold;
&::after {
content: "*";
color: #df2b2c;
}
}
.sub-title {
font-size: 1.2rem;
color: #999;
}
}
.sketch-list-container {
column-gap: 1rem;
flex-wrap: wrap;
.sketch-element {
width: 10rem;
height: 14.6rem;
border: 0.15rem solid #c7c7c7;
border-radius: 1.2rem;
position: relative;
overflow: hidden;
.img-src {
// height: 100%;
width: 100%;
}
.crop-tool {
position: absolute;
top: 0.4rem;
right: 0.4rem;
width: 2rem;
height: 2rem;
border-radius: 50%;
background-color: #000000;
}
}
}
}
}
.right { .right {
width: 55.2rem; width: 55.2rem;
flex-shrink: 0; flex-shrink: 0;
.form-container {
row-gap: 3rem;
.form-item {
.form-item-label {
font-size: 1.4rem;
font-weight: 400;
font-style: bold;
margin-bottom: 0.6rem;
line-height: 1.5;
&.with-tip {
display: flex;
align-items: center;
column-gap: 0.8rem;
}
.help-text {
font-size: 1rem;
color: #999;
}
}
.form-item-value {
border: 0.16rem solid #d1d1d1;
border-radius: 1.2rem;
position: relative;
padding: 1.6rem;
box-sizing: border-box;
font-size: 1.2rem;
color: #000;
&.no-border {
border: none;
padding: 0;
}
&.price {
column-gap: 0.6rem;
}
:deep(.ant-input),
:deep(.ant-input-affix-wrapper),
:deep(.ant-input-textarea textarea) {
border: none;
padding: 0;
font-size: 1.2rem;
color: #000;
box-shadow: none;
}
:deep(.ant-input) {
line-height: 1.5;
}
:deep(.ant-input-show-count-suffix) {
color: #df2c2c;
font-size: 1rem;
}
:deep(textarea.ant-input) {
resize: none;
min-height: 5.4rem;
padding-bottom: 1.8rem;
line-height: 1.5;
}
:deep(.ant-input-textarea-show-count) {
position: relative;
&::after {
float: none;
position: absolute;
color: #df2c2c;
bottom: 0;
right: 1.6rem;
}
}
}
}
}
.license-note {
padding: 1.6rem;
column-gap: 1.6rem;
background: #f7f7f7;
border-radius: 0.8rem;
.info-icon {
width: 2.4rem;
height: 2.4rem;
flex-shrink: 0;
}
.note-copy {
font-size: 1.2rem;
line-height: 1.5;
color: #000;
font-weight: 400;
font-style: medium;
a {
color: #0080ed;
text-decoration: underline;
margin-left: 0.4rem;
}
}
}
.page-control { .page-control {
justify-content: flex-end; justify-content: flex-end;
margin-top: 4rem; margin-top: 4rem;

View File

@@ -0,0 +1,47 @@
export type RadioOption = {
name: string | number
value: string | number | boolean
key: string
optype: boolean
}
export type TopImageType = "sketch" | "mainProductImage" | "cover"
export type CropType = TopImageType | "apparel"
export type ListingItem = {
designItemId: number | string | null
sketch: string | null
mainProductImage: string
cover: string
productImage: string[]
apparelSketch: string[]
productName: string
price: string
desc: string
gender: string
category: string[] | null
prodImageList: Array<{
url: string
selected?: boolean
}>
sketchList: Array<{ url: string | null }>
}
export type ListingDetailImage = {
category?: string | null
imageUrl?: string | null
isSelected?: boolean | number | string | null
sortOrder?: number | null
}
export type ListingDetailResponse = {
id?: number | string | null
title?: string | null
description?: string | null
price?: number | string | null
designFor?: string | null
productCategory?: string | string[] | null
images?: ListingDetailImage[] | null
}
export type StatusType = "draft" | "publish"

View File

@@ -41,8 +41,11 @@ const getCreateList = ()=>{
}) })
} }
const selectCollectionItem = (item:any)=>{ const selectCollectionItem = (item:any)=>{
console.log(item)
if(item.userLikeGroupVO?.groupDetails?.length > 0){
emit('selectCollectionItem',item) emit('selectCollectionItem',item)
} }
}
onMounted(()=>{ onMounted(()=>{
getCreateList() getCreateList()
}) })
@@ -55,9 +58,12 @@ defineExpose({getCreateList})
<div class="list"> <div class="list">
<div v-for="(item,index) in list" :key="index" class="item" @click="selectCollectionItem(item)"> <div v-for="(item,index) in list" :key="index" class="item" @click="selectCollectionItem(item)">
<div class="imgList"> <div class="imgList">
<div v-for="(img,index) in item.userLikeGroupVO?.groupDetails" :key="index" class="img"> <div v-if="item.userLikeGroupVO?.groupDetails?.length > 0" v-for="(img,index) in item.userLikeGroupVO?.groupDetails" :key="index" class="img">
<img :src="img.url" alt=""> <img :src="img.url" alt="">
</div> </div>
<div v-else class="img null">
<img src="@/assets/images/seller/selectCollectionNullStatus.png" alt="">
</div>
</div> </div>
<div class="detail"> <div class="detail">
<div class="name">{{item.name}}</div> <div class="name">{{item.name}}</div>
@@ -123,6 +129,12 @@ defineExpose({getCreateList})
border-radius: 1rem; border-radius: 1rem;
overflow: hidden; overflow: hidden;
background-color: #fff; background-color: #fff;
&.null{
background-color: transparent;
> img{
object-fit: contain;
}
}
> img{ > img{
object-fit: cover; object-fit: cover;
height: 100%; height: 100%;

View File

@@ -45,8 +45,8 @@ const next = ()=>{
router.push({ router.push({
path:'/home/seller/myListings/edit', path:'/home/seller/myListings/edit',
state: { state: {
id:route.params.collectionId,
designItemIds, designItemIds,
type:'create'
} }
}) })
} }
@@ -83,7 +83,7 @@ const setDomSize = (width: number)=>{
if(!listingsBoxRef.value)return if(!listingsBoxRef.value)return
let listDom = listingsBoxRef.value.querySelector('.list') let listDom = listingsBoxRef.value.querySelector('.list')
let listItemDom = listDom.querySelector('.item') let listItemDom = listDom.querySelector('.item')
let offsetWidth = listItemDom.getBoundingClientRect().width let offsetWidth = listItemDom?.getBoundingClientRect?.()?.width
let lineNum = Math.floor(width / offsetWidth) let lineNum = Math.floor(width / offsetWidth)
let itemNum = Math.floor((width - (lineNum - 1) * parseInt(gap.value[domSize.value])) / offsetWidth) let itemNum = Math.floor((width - (lineNum - 1) * parseInt(gap.value[domSize.value])) / offsetWidth)
listDom.style.maxWidth = ((itemNum - 1) * parseInt(gap.value[domSize.value]) + itemNum * (offsetWidth)) + 'px' listDom.style.maxWidth = ((itemNum - 1) * parseInt(gap.value[domSize.value]) + itemNum * (offsetWidth)) + 'px'

View File

@@ -5,6 +5,8 @@ import contentItem from "./contentItem.vue"
import selectMenu from '@/component/modules/selectMenu.vue' import selectMenu from '@/component/modules/selectMenu.vue'
import deleteDrafts from './deleteDrafts.vue' import deleteDrafts from './deleteDrafts.vue'
import { Https } from '@/tool/https' import { Https } from '@/tool/https'
import { message } from 'ant-design-vue'
import { useRouter } from 'vue-router'
//const props = defineProps({ //const props = defineProps({
//}) //})
@@ -63,11 +65,14 @@ const domSizeList = ref([
const visible = ref(false) const visible = ref(false)
const deleteDraftsRef = ref(null) const deleteDraftsRef = ref(null)
const router = useRouter()
const deleteDraft = (item: any)=>{ const deleteDraft = (item: any)=>{
deleteDraftsRef.value.open(()=>{ deleteDraftsRef.value.open(item,()=>{
putListingStatus(item,2).then(()=>{
list2.value = list2.value.filter((v: any)=>v.id != item.id) list2.value = list2.value.filter((v: any)=>v.id != item.id)
}) })
})
} }
const vObserve = { const vObserve = {
@@ -99,6 +104,7 @@ const getPublishedData = async ()=>{
} }
await getPublishList(value).then((res)=>{ await getPublishList(value).then((res)=>{
if(res.content.length == 0)publishData.isNoData = true if(res.content.length == 0)publishData.isNoData = true
publishData.pageNum += 1
list.value.push(...res.content) list.value.push(...res.content)
}) })
publishData.isShowMark = false publishData.isShowMark = false
@@ -113,6 +119,7 @@ const getNoPublishedData = async ()=>{
} }
await getPublishList(value).then((res)=>{ await getPublishList(value).then((res)=>{
if(res.content.length == 0)noPublishData.isNoData = true if(res.content.length == 0)noPublishData.isNoData = true
noPublishData.pageNum += 1
list2.value.push(...res.content) list2.value.push(...res.content)
}) })
noPublishData.isShowMark = false noPublishData.isShowMark = false
@@ -127,28 +134,46 @@ const getPublishList = (data:any)=>{
}) })
} }
const putListingStatus = ()=>{ const putListingStatus = async (item,status:number)=>{
return new Promise<void>((resolve, reject) => {
let data = { let data = {
id:'', id:item.id,
status:'', status:status,
} }
Https.axiosPut(Https.httpUrls.putListingStatus,data).then((res:any)=>{ Https.axiosPut(Https.httpUrls.putListingStatus,data).then((res:any)=>{
resolve(res)
}).catch((err:any)=>{
reject(err)
})
}) })
} }
const draftListing = (item: any)=>{ const draftListing = async (item: any)=>{
//数组前面添加item //数组前面添加item
await putListingStatus(item,0).then(()=>{
list2.value.unshift(item) list2.value.unshift(item)
list.value = list.value.filter((v: any)=>v.id != item.id) list.value = list.value.filter((v: any)=>v.id != item.id)
})
message.success('Product moved to drafts and stats reset.')
} }
const publishListing = (item: any)=>{ const publishListing = async (item: any)=>{
await putListingStatus(item,1).then(()=>{
list.value.unshift(item) list.value.unshift(item)
list2.value = list2.value.filter((v: any)=>v.id != item.id) list2.value = list2.value.filter((v: any)=>v.id != item.id)
})
message.success('Item is now live on the Marketplace.')
} }
const editListing = (item: any)=>{ const editListing = (item: any)=>{
router.push({
path:'/home/seller/myListings/edit',
state: {
id:item.id,
type:'edit'
}
})
} }
@@ -167,7 +192,7 @@ const setDomSize = (width: number)=>{
if(!listingsBoxRef.value)return if(!listingsBoxRef.value)return
let listDom = listingsBoxRef.value.querySelector('.list') let listDom = listingsBoxRef.value.querySelector('.list')
let listItemDom = listDom.querySelector('.item') let listItemDom = listDom.querySelector('.item')
let offsetWidth = listItemDom.getBoundingClientRect().width let offsetWidth = listItemDom?.getBoundingClientRect?.()?.width
let lineNum = Math.floor(width / offsetWidth) let lineNum = Math.floor(width / offsetWidth)
let itemNum = Math.floor((width - (lineNum - 1) * parseInt(gap.value[domSize.value])) / offsetWidth) let itemNum = Math.floor((width - (lineNum - 1) * parseInt(gap.value[domSize.value])) / offsetWidth)
listDom.style.maxWidth = ((itemNum - 1) * parseInt(gap.value[domSize.value]) + itemNum * (offsetWidth)) + 'px' listDom.style.maxWidth = ((itemNum - 1) * parseInt(gap.value[domSize.value]) + itemNum * (offsetWidth)) + 'px'

View File

@@ -32,7 +32,7 @@ const {} = toRefs(data);
<template> <template>
<div class="item" :draging="true" :class="domSize"> <div class="item" :draging="true" :class="domSize">
<div class="imgBox"> <div class="imgBox">
<img src="" alt=""> <img :src="item.cover" alt="">
<div class="maskBtn"> <div class="maskBtn">
<div @click="$emit('editListing',item)"> <div @click="$emit('editListing',item)">
<svgIcon name="seller-edit" :size="domSize == 'Small'?32:domSize == 'Medium'?40:48" /> <svgIcon name="seller-edit" :size="domSize == 'Small'?32:domSize == 'Medium'?40:48" />
@@ -50,21 +50,21 @@ const {} = toRefs(data);
</div> </div>
<div class="detail"> <div class="detail">
<div class="left"> <div class="left">
<div class="name">item name</div> <div class="name">{{item.title}}</div>
<div class="price">$1123</div> <div class="price">${{item.price}}</div>
</div> </div>
<div class="right"> <div class="right">
<div class="detailItem" v-if="type == 'listings'"> <div class="detailItem" v-if="type == 'listings'">
<div class="shopping1"> <div class="shopping1">
<i class="fi fi-rr-shopping-bag-add"></i> <i class="fi fi-rr-shopping-bag-add"></i>
</div> </div>
<span>123</span> <span>{{ item.salesVolume || 0 }}</span>
</div> </div>
<div class="detailItem" v-if="type == 'listings'"> <div class="detailItem" v-if="type == 'listings'">
<div class="eye1"> <div class="eye1">
<i class="fi fi-rs-eye"></i> <i class="fi fi-rs-eye"></i>
</div> </div>
<span>123</span> <span>{{ item.viewCount }}</span>
</div> </div>
<div class="detailItem drafts" v-if="type == 'drafts'" @click="$emit('deleteDraft',item)"> <div class="detailItem drafts" v-if="type == 'drafts'" @click="$emit('deleteDraft',item)">
<div class=""> <div class="">
@@ -128,6 +128,11 @@ const {} = toRefs(data);
position: relative; position: relative;
height: var(--itemImgHeight); height: var(--itemImgHeight);
width: 100%; width: 100%;
> img{
width: 100%;
height: 100%;
object-fit: cover;
}
> .maskBtn{ > .maskBtn{
position: absolute; position: absolute;
width: 100%; width: 100%;
@@ -182,6 +187,7 @@ const {} = toRefs(data);
gap: var(--detailRightItemGap); gap: var(--detailRightItemGap);
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
align-items: center;
> div{ > div{
color: var(--rightColor); color: var(--rightColor);
display: flex; display: flex;

View File

@@ -15,12 +15,13 @@ const router = useRouter()
const {t} = useI18n() const {t} = useI18n()
let data = reactive({ let data = reactive({
}) })
const item = ref<any>()
const fun = ref(null) const fun = ref(null)
let deleteDraftsRef = ref(null) let deleteDraftsRef = ref(null)
const open = (deleteFun)=>{ const open = (data:any,deleteFun)=>{
item.value = data
fun.value = deleteFun fun.value = deleteFun
emit('update:visible', true) emit('update:visible', true)
} }
@@ -35,6 +36,7 @@ const deleteDrafts = ()=>{
const cleardata = ()=>{ const cleardata = ()=>{
item.value = null
emit('update:visible', false) emit('update:visible', false)
} }
@@ -78,11 +80,11 @@ const { showAgain } = toRefs(data);
</div> </div>
<div class="deleteContent"> <div class="deleteContent">
<div class="img"> <div class="img">
<img src="" alt=""> <img :src="item?.value?.cover" alt="">
</div> </div>
<div class="detail"> <div class="detail">
<div class="name">Item Name</div> <div class="name">{{ item?.value?.title }}</div>
<div class="price">HK$392.00 · Draft</div> <div class="price">HK${{ item?.value?.price }} · Draft</div>
</div> </div>
</div> </div>
<div class="btnBox"> <div class="btnBox">