<!-- 管理后台用户管理页，负责用户创建、状态维护、每日限额和积分发放。 -->
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, reactive, ref } from "vue";
import { ElMessageBox, type FormInstance } from "element-plus";
import http, { apiError } from "@/api/http";
import AppIcon from "@/components/AppIcon.vue";
import AdminNumberInput from "@/components/AdminNumberInput.vue";
import PageHeader from "@/components/PageHeader.vue";
import { useToastStore } from "@/stores/toast";

const toast = useToastStore(),
  loading = ref(false),
  items = ref<Record<string, any>[]>([]),
  total = ref(0),
  selected = ref<Record<string, any>[]>([]),
  dialog = ref<"single" | "batch" | "">(""),
  pointGrantOpen = ref(false),
  pointGrantTarget = ref<Record<string, any>>(),
  grantingPoints = ref(false),
  editingLimitId = ref(""),
  editingLimitValue = ref<number | null>(null),
  savingLimitId = ref("");
const query = reactive({ keyword: "", enabled: "", page: 1, page_size: 15 });
const form = reactive({
  account: "",
  prefix: "",
  start_sequence: "001",
  end_sequence: "001",
  password: "",
  daily_limit: 9999,
});
const pointGrantForm = reactive({ points: null as number | null, reason: "", request_id: "" });
const formRef = ref<FormInstance>();
const dialogOpen = computed({
  get: () => dialog.value !== "",
  set: (value: boolean) => {
    if (!value) dialog.value = "";
  },
});
const canGrantPoints = computed(
  () =>
    Number.isFinite(Number(pointGrantForm.points)) &&
    Number(pointGrantForm.points) > 0 &&
    Boolean(pointGrantForm.reason.trim()),
);
const batchPreview = computed(() => {
  const prefix = form.prefix.trim();
  const startRaw = form.start_sequence.trim();
  const endRaw = form.end_sequence.trim();
  if (!prefix) return { error: "请输入账号前缀", count: 0, first: "", last: "" };
  if (!/^\d+$/.test(startRaw) || !/^\d+$/.test(endRaw)) {
    return { error: "起始序号和终止序号必须为数字", count: 0, first: "", last: "" };
  }
  const start = Number(startRaw);
  const end = Number(endRaw);
  if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end)) {
    return { error: "序号超出有效范围", count: 0, first: "", last: "" };
  }
  if (start > end) return { error: "起始序号不能大于终止序号", count: 0, first: "", last: "" };
  const count = end - start + 1;
  if (count > 50) return { error: "每次最多批量创建 50 个用户", count, first: "", last: "" };
  const width = Math.max(startRaw.length, endRaw.length);
  return {
    error: "",
    count,
    first: `${prefix}-${String(start).padStart(width, "0")}`,
    last: `${prefix}-${String(end).padStart(width, "0")}`,
  };
});
async function load(silent = false) {
  if (silent && editingLimitId.value) return;
  if (!silent) loading.value = true;
  try {
    const { data } = await http.get("/users", { params: query });
    items.value = data.data.items;
    total.value = data.data.total;
  } catch (e) {
    showError(e);
  } finally {
    loading.value = false;
  }
}
function editDailyLimit(row: Record<string, any>) {
  editingLimitId.value = row.id;
  editingLimitValue.value = Number(row.daily_limit);
}
function cancelDailyLimitEdit() {
  editingLimitId.value = "";
  editingLimitValue.value = null;
}
function blurLimitInput(event: KeyboardEvent) {
  (event.target as HTMLInputElement).blur();
}
async function saveDailyLimit(row: Record<string, any>) {
  if (editingLimitId.value !== row.id) return;
  const value = Number(editingLimitValue.value);
  if (!Number.isFinite(value) || value < 0) {
    toast.warning("每日限额不能小于 0");
    cancelDailyLimitEdit();
    return;
  }
  if (value === Number(row.daily_limit)) {
    cancelDailyLimitEdit();
    return;
  }
  savingLimitId.value = row.id;
  try {
    await http.patch("/users/batch", {
      ids: [row.id],
      daily_limit: value.toFixed(2),
      reason: "管理员原地修改每日限额",
    });
    row.daily_limit = value;
    toast.success("每日限额已保存");
  } catch (e) {
    showError(e);
  } finally {
    savingLimitId.value = "";
    cancelDailyLimitEdit();
  }
}
function open(type: "single" | "batch") {
  Object.assign(form, {
    account: "",
    prefix: "",
    start_sequence: "001",
    end_sequence: "001",
    password: "",
    daily_limit: 9999,
  });
  dialog.value = type;
}
async function create() {
  if (!(await formRef.value?.validate().catch(() => false))) return;
  if (dialog.value === "batch" && batchPreview.value.error) {
    toast.warning(batchPreview.value.error);
    return;
  }
  loading.value = true;
  try {
    if (dialog.value === "single")
      await http.post("/users", {
        account: form.account,
        password: form.password,
        daily_limit: form.daily_limit,
      });
    else
      await http.post("/users/batch", {
        prefix: form.prefix,
        start_sequence: form.start_sequence,
        end_sequence: form.end_sequence,
        password: form.password,
        daily_limit: form.daily_limit,
      });
    toast.success("用户创建成功");
    dialog.value = "";
    load();
  } catch (e) {
    showError(e);
  } finally {
    loading.value = false;
  }
}
async function toggle(row: Record<string, any>, enabled: boolean) {
  try {
    await ElMessageBox.confirm(
      `${enabled ? "启用" : "停用"}用户 ${row.account}（${row.username}）？停用后会撤销其有效会话。`,
      "确认状态变更",
      { type: "warning" },
    );
    await http.patch("/users/batch", {
      ids: [row.id],
      enabled,
      reason: `管理员${enabled ? "启用" : "停用"}用户`,
    });
    row.enabled = enabled;
    toast.success("用户状态已更新");
  } catch (e) {
    row.enabled = !enabled;
    if (e !== "cancel" && e !== "close") showError(e);
  }
}
async function adjustLimit() {
  try {
    const { value } = await ElMessageBox.prompt(
      "请输入新的每日限额（0 表示禁止消耗，9999 表示不限制）",
      "批量调整每日限额",
      {
        inputType: "text",
        inputValue: "9999",
        inputValidator: (v) => Number(v) >= 0 || "限额不能小于 0",
      },
    );
    const reason = await ElMessageBox.prompt("请填写调整原因", "填写审计原因", {
      inputValidator: (v) => Boolean(v.trim()) || "调整原因不能为空",
    });
    await http.patch("/users/batch", {
      ids: selected.value.map((i) => i.id),
      daily_limit: Number(value).toFixed(2),
      reason: reason.value,
    });
    toast.success("每日限额已调整");
    selected.value = [];
    load();
  } catch (e) {
    if (e !== "cancel" && e !== "close") showError(e);
  }
}
/** 打开单用户积分发放弹窗，并为本次操作生成幂等请求号。 */
function openPointGrant(row: Record<string, any>) {
  pointGrantTarget.value = row;
  Object.assign(pointGrantForm, { points: null, reason: "", request_id: crypto.randomUUID() });
  pointGrantOpen.value = true;
}
/** 校验正数积分并通过账务接口完成入账，成功后同步列表余额。 */
async function grantPoints() {
  if (!pointGrantTarget.value || !canGrantPoints.value) return;
  grantingPoints.value = true;
  try {
    const points = Number(pointGrantForm.points).toFixed(2);
    const { data } = await http.post(`/users/${pointGrantTarget.value.id}/points`, {
      request_id: pointGrantForm.request_id,
      points,
      reason: pointGrantForm.reason.trim(),
    });
    pointGrantTarget.value.point_balance = data.data.balance;
    toast.success(`已增加 ${points} 积分`);
    pointGrantOpen.value = false;
  } catch (e) {
    showError(e);
  } finally {
    grantingPoints.value = false;
  }
}
async function remove(ids: string[]) {
  try {
    const reason = await ElMessageBox.prompt(
      `确认软删除 ${ids.length} 个用户？其订单、流水和使用记录将保留。`,
      "确认删除",
      {
        type: "warning",
        inputPlaceholder: "请输入删除原因",
        inputValidator: (v) => Boolean(v.trim()) || "删除原因不能为空",
        confirmButtonText: "确认删除",
      },
    );
    await http.delete("/users/batch", { data: { ids, reason: reason.value } });
    toast.success("用户已删除");
    selected.value = [];
    load();
  } catch (e) {
    if (e !== "cancel" && e !== "close") showError(e);
  }
}
const fmt = (v: any) =>
  v
    ? new Date(v).toLocaleString("zh-CN", {
        timeZone: "Asia/Shanghai",
        hour12: false,
      })
    : "从未在线";
function showError(e: unknown) {
  const d = apiError(e);
  toast.error(d.message, d.copyText);
}
const timer = window.setInterval(() => load(true), 300000);
onMounted(load);
onBeforeUnmount(() => clearInterval(timer));
</script>
<template>
  <div class="page-stack management-page">
    <PageHeader
      title="用户管理"
      description="管理 WEB 用户账号、每日限额和启用状态"
      ><el-button @click="open('batch')"> <AppIcon name="UserFilled" />批量新建 </el-button
      ><el-button
        type="primary"
        @click="open('single')"
      >
        <AppIcon name="Plus" />新建用户
      </el-button></PageHeader
    >
    <section class="panel filter-bar">
      <el-input
        v-model="query.keyword"
        clearable
        placeholder="账号、用户名或 UID"
      /><el-select
        v-model="query.enabled"
        clearable
        placeholder="启用状态"
        ><el-option
          label="已启用"
          value="true" /><el-option
          label="已停用"
          value="false" /></el-select
      ><el-button
        type="primary"
        @click="
          query.page = 1;
          load();
        "
        >查询</el-button
      ><el-button
        @click="
          query.keyword = '';
          query.enabled = '';
          query.page = 1;
          load();
        "
        >重置</el-button
      >
    </section>
    <section
      v-if="selected.length"
      class="batch-toolbar"
    >
      <strong>已选择 {{ selected.length }} 项</strong><el-button @click="selected = []">取消选择</el-button
      ><el-button @click="adjustLimit">调整每日限额</el-button
      ><el-button
        type="danger"
        @click="remove(selected.map((i) => i.id))"
        >批量删除</el-button
      >
    </section>
    <section
      class="panel table-panel"
      v-loading="loading"
    >
      <el-table
        class="desktop-user-table"
        :data="items"
        @selection-change="selected = $event"
        ><el-table-column
          type="selection"
          width="46"
        /><el-table-column
          prop="account"
          label="账号"
          min-width="150"
          show-overflow-tooltip
        /><el-table-column
          prop="username"
          label="用户名"
          min-width="170"
        /><el-table-column
          prop="uid"
          label="UID"
          min-width="125"
        /><el-table-column
          label="积分余额"
          min-width="120"
          ><template #default="{ row }">{{ Number(row.point_balance).toFixed(2) }}</template></el-table-column
        ><el-table-column
          label="今日消耗"
          min-width="120"
          ><template #default="{ row }">{{ Number(row.today_consumption).toFixed(2) }}</template></el-table-column
        ><el-table-column
          label="每日限额"
          min-width="170"
          ><template #default="{ row }">
            <div class="limit-cell">
              <AdminNumberInput
                v-if="editingLimitId === row.id"
                v-model="editingLimitValue"
                class="limit-input"
                :min="0"
                :precision="2"
                :disabled="savingLimitId === row.id"
                autofocus
                @blur="saveDailyLimit(row)"
              /><template v-else
                ><span>{{ Number(row.daily_limit) === 9999 ? "不限制" : Number(row.daily_limit).toFixed(2) }}</span
                ><el-button
                  class="limit-edit-button"
                  text
                  circle
                  type="primary"
                  title="编辑每日限额"
                  @click="editDailyLimit(row)"
                >
                  <AppIcon
                    name="Edit"
                    :size="15"
                  /> </el-button
              ></template>
            </div> </template></el-table-column
        ><el-table-column
          label="启用状态"
          width="96"
          ><template #default="{ row }"
            ><el-switch
              :model-value="row.enabled"
              @change="toggle(row, Boolean($event))" /></template></el-table-column
        ><el-table-column
          label="最后在线时间"
          min-width="190"
          ><template #default="{ row }">{{ fmt(row.last_online_at) }}</template></el-table-column
        ><el-table-column
          label="操作"
          width="150"
          ><template #default="{ row }"
            ><el-button
              link
              type="primary"
              :disabled="!row.enabled"
              :title="row.enabled ? '增加积分' : '停用用户不可增加积分'"
              @click="openPointGrant(row)"
              >增加积分</el-button
            ><el-button
              link
              type="danger"
              @click="remove([row.id])"
              >删除</el-button
            ></template
          ></el-table-column
        ></el-table
      >
      <div
        v-if="items.length"
        class="mobile-card-list"
      >
        <article
          v-for="row in items"
          :key="row.id"
          class="data-card"
        >
          <dl>
            <dt>账号</dt>
            <dd>{{ row.account }}</dd>
            <dt>用户名</dt>
            <dd>{{ row.username }}</dd>
            <dt>UID</dt>
            <dd>{{ row.uid }}</dd>
            <dt>积分余额</dt>
            <dd>{{ Number(row.point_balance).toFixed(2) }}</dd>
            <dt>今日消耗</dt>
            <dd>{{ Number(row.today_consumption).toFixed(2) }}</dd>
            <dt>每日限额</dt>
            <dd>
              <div class="limit-cell">
                <AdminNumberInput
                  v-if="editingLimitId === row.id"
                  v-model="editingLimitValue"
                  class="limit-input"
                  :min="0"
                  :precision="2"
                  :disabled="savingLimitId === row.id"
                  autofocus
                  @blur="saveDailyLimit(row)"
                /><template v-else
                  ><span>{{ Number(row.daily_limit) === 9999 ? "不限制" : Number(row.daily_limit).toFixed(2) }}</span
                  ><el-button
                    class="limit-edit-button"
                    text
                    circle
                    type="primary"
                    title="编辑每日限额"
                    @click="editDailyLimit(row)"
                  >
                    <AppIcon
                      name="Edit"
                      :size="15"
                    /> </el-button
                ></template>
              </div>
            </dd>
            <dt>最后在线</dt>
            <dd>{{ fmt(row.last_online_at) }}</dd>
          </dl>
          <div class="card-actions">
            <el-switch
              :model-value="row.enabled"
              @change="toggle(row, Boolean($event))"
            />
            <el-button
              type="primary"
              plain
              :disabled="!row.enabled"
              @click="openPointGrant(row)"
              >增加积分</el-button
            >
            <el-button
              type="danger"
              plain
              @click="remove([row.id])"
              >删除</el-button
            >
          </div>
        </article>
      </div>
      <el-empty
        v-if="!loading && !items.length"
        description="暂无用户"
      /><el-pagination
        v-if="total > query.page_size"
        v-model:current-page="query.page"
        background
        layout="total,prev,pager,next"
        :total="total"
        @current-change="load"
      />
    </section>
    <el-dialog
      v-model="pointGrantOpen"
      title="增加用户积分"
      width="min(480px, calc(100vw - 32px))"
      :close-on-click-modal="!grantingPoints"
      :show-close="!grantingPoints"
    >
      <div
        v-if="pointGrantTarget"
        class="point-grant-summary"
      >
        <strong>{{ pointGrantTarget.account }}（{{ pointGrantTarget.username }}）</strong>
        <span>当前余额 {{ Number(pointGrantTarget.point_balance).toFixed(2) }} 积分</span>
      </div>
      <el-form label-position="top">
        <el-form-item label="增加积分">
          <AdminNumberInput
            v-model="pointGrantForm.points"
            :min="0.01"
            :precision="2"
            :disabled="grantingPoints"
            placeholder="请输入大于 0 的积分数量"
          />
          <div class="field-helper">仅支持增加积分，到账后会写入用户积分变动记录</div>
        </el-form-item>
        <el-form-item label="操作原因">
          <el-input
            v-model.trim="pointGrantForm.reason"
            maxlength="200"
            show-word-limit
            :disabled="grantingPoints"
            placeholder="用于管理后台审计，不会向用户展示"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button
          :disabled="grantingPoints"
          @click="pointGrantOpen = false"
          >取消</el-button
        >
        <el-button
          type="primary"
          :loading="grantingPoints"
          :disabled="!canGrantPoints"
          @click="grantPoints"
          >确认增加</el-button
        >
      </template>
    </el-dialog>
    <el-dialog
      v-model="dialogOpen"
      :title="dialog === 'single' ? '新建用户' : '批量新建用户'"
      width="min(560px, calc(100vw - 32px))"
      ><el-form
        ref="formRef"
        :model="form"
        label-position="top"
        ><el-form-item
          v-if="dialog === 'single'"
          label="账号"
          prop="account"
          :rules="[{ required: true, message: '请输入账号' }]"
        >
          <el-input
            v-model.trim="form.account"
            placeholder="用于用户登录，创建后不可修改"
          />
          <div class="field-helper">系统将自动生成随机用户名，账号创建后不可修改</div>
        </el-form-item>
        <template v-else>
          <div class="dialog-form-grid batch-account-grid">
            <el-form-item
              class="dialog-field--full"
              label="账号前缀"
              prop="prefix"
              :rules="[{ required: true, message: '请输入账号前缀' }]"
            >
              <el-input
                v-model.trim="form.prefix"
                placeholder="例如 RR"
              />
            </el-form-item>
            <el-form-item
              label="起始序号"
              prop="start_sequence"
              :rules="[
                { required: true, message: '请输入起始序号' },
                { pattern: /^\d+$/, message: '起始序号必须为数字' },
              ]"
            >
              <el-input
                v-model.trim="form.start_sequence"
                placeholder="例如 001"
              />
            </el-form-item>
            <el-form-item
              label="终止序号"
              prop="end_sequence"
              :rules="[
                { required: true, message: '请输入终止序号' },
                { pattern: /^\d+$/, message: '终止序号必须为数字' },
              ]"
            >
              <el-input
                v-model.trim="form.end_sequence"
                placeholder="例如 050"
              />
            </el-form-item>
          </div>
          <el-alert
            class="batch-preview"
            :closable="false"
            :type="batchPreview.error ? 'error' : 'info'"
            :title="
              batchPreview.error
                ? batchPreview.error
                : `将创建 ${batchPreview.count} 个账号：${batchPreview.first}${batchPreview.count > 1 ? ` 至 ${batchPreview.last}` : ''}`
            "
          />
          <div class="field-helper batch-helper">
            账号之间使用短横线分隔；序号会保留输入位数，每次最多创建 50 个用户
          </div>
        </template>
        <el-form-item
          label="密码"
          prop="password"
          :rules="[{ required: true, min: 8, message: '密码至少 8 个字符' }]"
          ><el-input
            v-model="form.password"
            type="password"
            show-password
            autocomplete="new-password" /></el-form-item
        ><el-form-item label="每日限额"
          ><AdminNumberInput
            v-model="form.daily_limit"
            :min="0"
            :precision="2"
          />
          <div class="field-helper">0 表示当日禁止消耗，9999 表示不限制</div>
        </el-form-item></el-form
      ><template #footer
        ><el-button @click="dialog = ''">取消</el-button
        ><el-button
          type="primary"
          :loading="loading"
          :disabled="dialog === 'batch' && Boolean(batchPreview.error)"
          @click="create"
          >创建</el-button
        ></template
      ></el-dialog
    >
  </div>
</template>

<style scoped>
.desktop-user-table {
  width: 100%;
}

.batch-preview {
  margin-bottom: 0;
}

.batch-helper {
  margin-bottom: 18px;
}

.limit-cell {
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 6px;
}

.limit-cell > span {
  min-width: 58px;
  color: var(--color-text-secondary);
  font-variant-numeric: tabular-nums;
}

.limit-cell .el-button {
  margin: 0;
}

.limit-edit-button {
  width: 28px;
  height: 28px;
  padding: 0;
}

.limit-input {
  width: 118px;
}

.limit-input :deep(input) {
  font-variant-numeric: tabular-nums;
}

.point-grant-summary {
  margin-bottom: var(--space-md);
  padding: var(--space-sm) var(--space-md);
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  color: var(--color-text);
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
}

.point-grant-summary span {
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
  font-variant-numeric: tabular-nums;
}

@media (max-width: 767px) {
  .limit-cell {
    justify-content: flex-end;
  }

  .limit-input {
    width: min(128px, 100%);
  }
}
</style>
